diff --git a/Api/Api.csproj b/Api/Api.csproj index 50ad0d7..580ca2e 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -7,6 +7,7 @@ + diff --git a/Api/KernelFunctions.cs b/Api/KernelFunctions.cs index 0a85dab..6773b08 100644 --- a/Api/KernelFunctions.cs +++ b/Api/KernelFunctions.cs @@ -21,10 +21,10 @@ namespace blazoract.Api { public class KernelFunction { - private CompositeKernel _kernel; - public KernelFunction(CompositeKernel kernel) + private KernelStore _kernels; + public KernelFunction(KernelStore kernels) { - this._kernel = kernel; + _kernels = kernels; } [FunctionName("RunCode")] @@ -33,9 +33,9 @@ public async Task RunCode( ILogger log) { string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); - var cell = JsonConvert.DeserializeObject(requestBody); - - var request = await _kernel.SendAsync(new SubmitCode(cell.Input), new CancellationToken()); + var executeRequest = JsonConvert.DeserializeObject(requestBody); + var kernel = _kernels.GetKernelForNotebook(executeRequest.NotebookId); + var request = await kernel.SendAsync(new SubmitCode(executeRequest.Code), CancellationToken.None); var result = new ExecuteResult(); request.KernelEvents.Subscribe(x => { @@ -62,7 +62,8 @@ public async Task GetCompletions( string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var completionRequest = JsonConvert.DeserializeObject(requestBody); - var request = await _kernel.SendAsync(new RequestCompletions(completionRequest.Code, new LinePosition(completionRequest.LineNumber - 1, completionRequest.Column - 1))); + var kernel = _kernels.GetKernelForNotebook(completionRequest.NotebookId); + var request = await kernel.SendAsync(new RequestCompletions(completionRequest.Code, new LinePosition(completionRequest.LineNumber - 1, completionRequest.Column - 1))); var result = Array.Empty(); request.KernelEvents.Subscribe(x => { diff --git a/Api/KernelStore.cs b/Api/KernelStore.cs new file mode 100644 index 0000000..3b19761 --- /dev/null +++ b/Api/KernelStore.cs @@ -0,0 +1,42 @@ +using Microsoft.Azure.Functions.Extensions.DependencyInjection; +using Microsoft.DotNet.Interactive.CSharp; +using Microsoft.DotNet.Interactive; +using System.Collections.Concurrent; +using Microsoft.Extensions.Caching.Memory; +using System; + +[assembly: FunctionsStartup(typeof(blazoract.Api.Startup))] + +namespace blazoract.Api +{ + // To avoid interference across notebooks, we need a separate kernel instance per notebook. + // Set up a cache in memory that holds a certain maximum number of instances, and evicts + // entries if they become idle for longer than a certain period. + + public class KernelStore + { + const int MaxEntries = 1024; + const int MaxIdleMinutes = 15; + + private MemoryCache _kernelsCache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = MaxEntries + }); + + private ConcurrentDictionary _kernels + = new ConcurrentDictionary(); + + public CompositeKernel GetKernelForNotebook(string notebookId) + { + return _kernelsCache.GetOrCreate(notebookId, entry => + { + entry.SetSlidingExpiration(TimeSpan.FromMinutes(MaxIdleMinutes)); + entry.Size = 1; + return new CompositeKernel() + { + new CSharpKernel().UseDefaultFormatting().UseDotNetVariableSharing() + }; + }); + } + } +} diff --git a/Api/Startup.cs b/Api/Startup.cs index 5c67858..11a0a19 100644 --- a/Api/Startup.cs +++ b/Api/Startup.cs @@ -1,7 +1,5 @@ using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.DotNet.Interactive.CSharp; -using Microsoft.DotNet.Interactive; [assembly: FunctionsStartup(typeof(blazoract.Api.Startup))] @@ -11,9 +9,7 @@ public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { - builder.Services.AddSingleton(new CompositeKernel() { - new CSharpKernel().UseDefaultFormatting().UseDotNetVariableSharing() - }); + builder.Services.AddSingleton(); } } -} \ No newline at end of file +} diff --git a/Client/Data/NotebookService.cs b/Client/Data/NotebookService.cs index bd00964..79e1201 100644 --- a/Client/Data/NotebookService.cs +++ b/Client/Data/NotebookService.cs @@ -30,7 +30,7 @@ public NotebookService(ILocalStorageService storage, HttpClient http) var initialContent = new List(); for (var i = 0; i < 100; i++) { - initialContent.Add(new Cell($"{i} * {i}", i)); + initialContent.Add(new Cell(id, $"{i} * {i}", i)); } notebook.Cells = initialContent; _storage.SetItemAsync("_default_notebook", notebook); @@ -57,7 +57,7 @@ public async Task CreateNewNotebook() var id = Guid.NewGuid().ToString("N"); var title = "New notebook"; var notebook = new Notebook(title, id); - notebook.Cells = new List() { new Cell("", 0) }; + notebook.Cells = new List() { new Cell(id, "// Type your code here", 0) }; await _storage.SetItemAsync(id, notebook); var notebooks = await _storage.GetItemAsync>("blazoract-notebooks") ?? new List(); @@ -71,7 +71,7 @@ public async Task CreateNewNotebook() public async Task AddCell(string id, string content, CellType type, int position) { var notebook = await GetById(id); - notebook.Cells.Insert(position, new Cell(content, position, type)); + notebook.Cells.Insert(position, new Cell(id, content, position, type)); await _storage.SetItemAsync(id, notebook); OnChange.Invoke(); return notebook; diff --git a/Client/Shared/CodeCell.razor b/Client/Shared/CodeCell.razor index 865def5..1841063 100644 --- a/Client/Shared/CodeCell.razor +++ b/Client/Shared/CodeCell.razor @@ -62,7 +62,7 @@ try { isEvaluating = true; - var request = new ExecuteRequest(cell.Content); + var request = new ExecuteRequest(cell.NotebookId, cell.Content); var response = await Http.PostAsJsonAsync("api/code/run", request); result = await response.Content.ReadFromJsonAsync(); } @@ -76,6 +76,7 @@ { var response = await Http.PostAsJsonAsync("api/code/completions", new GetCompletionsRequest { + NotebookId = cell.NotebookId, Code = value, Column = position.Column, LineNumber = position.LineNumber diff --git a/Shared/Cell.cs b/Shared/Cell.cs index dec6ff3..3142633 100644 --- a/Shared/Cell.cs +++ b/Shared/Cell.cs @@ -4,12 +4,16 @@ namespace blazoract.Shared { public class Cell { - public Cell(string content, int position, CellType type = CellType.Code) + public Cell(string notebookId, string content, int position, CellType type = CellType.Code) { + NotebookId = notebookId; Content = content; Position = position; Type = type; } + + public string NotebookId { get; set; } + public string Content { get; set; } public CellType Type { get; set; } diff --git a/Shared/ExecuteRequest.cs b/Shared/ExecuteRequest.cs index af2c466..646e3a8 100644 --- a/Shared/ExecuteRequest.cs +++ b/Shared/ExecuteRequest.cs @@ -4,10 +4,14 @@ namespace blazoract.Shared { public class ExecuteRequest { - public ExecuteRequest(string input) + public ExecuteRequest(string notebookId, string code) { - Input = input; + NotebookId = notebookId; + Code = code; } - public string Input { get; set; } + + public string NotebookId { get; set; } + + public string Code { get; set; } } } diff --git a/Shared/GetCompletionsRequest.cs b/Shared/GetCompletionsRequest.cs index d4b8d89..50bb560 100644 --- a/Shared/GetCompletionsRequest.cs +++ b/Shared/GetCompletionsRequest.cs @@ -2,6 +2,7 @@ { public class GetCompletionsRequest { + public string NotebookId { get; set; } public string Code { get; set; } public int LineNumber { get; set; } public int Column { get; set; }