Skip to content

Commit 2f4c8c0

Browse files
authored
Merge pull request #26 from xm-i/performance-optimize-highlight-service-lookup-18121708137471364968
⚡ Optimize style lookup in HighlightService using ToLookup
2 parents 40f0b98 + 0ec8d74 commit 2f4c8c0

3 files changed

Lines changed: 166 additions & 2 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Diagnostics;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using Moq;
5+
using RemoteLogViewer.Composition.Stores.Settings;
6+
using RemoteLogViewer.Core.Services;
7+
using RemoteLogViewer.Core.Services.Viewer;
8+
using RemoteLogViewer.Core.Stores.Settings;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
12+
namespace RemoteLogViewer.Core.Tests.Services.Viewer;
13+
14+
public class HighlightServiceBenchmark {
15+
private readonly ITestOutputHelper _output;
16+
17+
public HighlightServiceBenchmark(ITestOutputHelper output) {
18+
this._output = output;
19+
}
20+
21+
[Fact]
22+
public void BenchmarkCreateStyledLine() {
23+
var serviceProvider = CreateServiceProvider();
24+
var settingsStore = serviceProvider.GetRequiredService<SettingsStoreModel>();
25+
var highlightService = serviceProvider.GetRequiredService<HighlightService>();
26+
var grepCondition = serviceProvider.GetRequiredService<HighlightConditionModel>();
27+
28+
// Setup many rules
29+
var highlightSettings = settingsStore.SettingsModel.HighlightSettings;
30+
for (int i = 0; i < 100; i++) {
31+
var rule = highlightSettings.AddRule();
32+
var condition = rule.AddCondition();
33+
condition.Pattern.Value = $"test{i}";
34+
condition.PatternType.Value = HighlightPatternType.Exact;
35+
condition.HighlightOnlyMatch.Value = true;
36+
}
37+
38+
// Initialize CSS to populate _ruleWithClassName
39+
highlightService.CreateCss(".wrapper");
40+
41+
var content = string.Join(" ", Enumerable.Range(0, 1000).Select(i => $"test{i % 100}"));
42+
43+
// Warmup
44+
for (int i = 0; i < 10; i++) {
45+
highlightService.CreateStyledLine(content);
46+
}
47+
48+
var sw = Stopwatch.StartNew();
49+
int iterations = 100;
50+
for (int i = 0; i < iterations; i++) {
51+
highlightService.CreateStyledLine(content);
52+
}
53+
sw.Stop();
54+
55+
this._output.WriteLine($"Total time for {iterations} iterations: {sw.ElapsedMilliseconds}ms");
56+
this._output.WriteLine($"Average time per iteration: {sw.Elapsed.TotalMilliseconds / iterations}ms");
57+
}
58+
59+
private static IServiceProvider CreateServiceProvider() {
60+
var services = new ServiceCollection();
61+
services.AddSingleton(Mock.Of<ILogger<SettingsStoreModel>>());
62+
services.AddSingleton(Mock.Of<ILogger<WorkspaceService>>());
63+
services.AddSingleton(Mock.Of<ILogger<SettingsModel>>()); // Not really needed if we use implementation
64+
services.AddSingleton<WorkspaceService>();
65+
services.AddSingleton<SettingsStoreModel>();
66+
services.AddTransient<HighlightService>();
67+
68+
// Mock HighlightConditionModel (GREP)
69+
services.AddScoped<HighlightConditionModel>(sp => new HighlightConditionModel(sp));
70+
71+
// Setup for SettingsStoreModel
72+
services.AddSingleton<SettingsModel>();
73+
services.AddSingleton<HighlightSettingsModel>();
74+
services.AddSingleton<TextViewerSettingsModel>();
75+
services.AddSingleton<AdvancedSettingsModel>();
76+
services.AddScoped<HighlightRuleModel>();
77+
78+
return services.BuildServiceProvider();
79+
}
80+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Logging;
3+
using Moq;
4+
using RemoteLogViewer.Composition.Stores.Settings;
5+
using RemoteLogViewer.Core.Services;
6+
using RemoteLogViewer.Core.Services.Viewer;
7+
using RemoteLogViewer.Core.Stores.Settings;
8+
using Xunit;
9+
10+
namespace RemoteLogViewer.Core.Tests.Services.Viewer;
11+
12+
public class HighlightServiceTests {
13+
[Fact]
14+
public void CreateStyledLine_ShouldHighlightCorrectly() {
15+
var serviceProvider = CreateServiceProvider();
16+
var settingsStore = serviceProvider.GetRequiredService<SettingsStoreModel>();
17+
var highlightService = serviceProvider.GetRequiredService<HighlightService>();
18+
19+
var highlightSettings = settingsStore.SettingsModel.HighlightSettings;
20+
var rule1 = highlightSettings.AddRule();
21+
var cond1 = rule1.AddCondition();
22+
cond1.Pattern.Value = "test";
23+
cond1.PatternType.Value = HighlightPatternType.Exact;
24+
cond1.HighlightOnlyMatch.Value = true;
25+
26+
highlightService.CreateCss(".wrapper");
27+
28+
var result = highlightService.CreateStyledLine("this is a test line");
29+
30+
Assert.Contains("<span class=\"c0\">test</span>", result);
31+
}
32+
33+
[Fact]
34+
public void CreateStyledLine_OverlappingStyles_ShouldHandleCorrectly() {
35+
var serviceProvider = CreateServiceProvider();
36+
var settingsStore = serviceProvider.GetRequiredService<SettingsStoreModel>();
37+
var highlightService = serviceProvider.GetRequiredService<HighlightService>();
38+
39+
var highlightSettings = settingsStore.SettingsModel.HighlightSettings;
40+
41+
// Priority 0 (Lower number, higher priority)
42+
var rule0 = highlightSettings.AddRule();
43+
var cond0 = rule0.AddCondition();
44+
cond0.Pattern.Value = "abcde";
45+
cond0.PatternType.Value = HighlightPatternType.Exact;
46+
cond0.HighlightOnlyMatch.Value = true;
47+
48+
// Priority 1
49+
var rule1 = highlightSettings.AddRule();
50+
var cond1 = rule1.AddCondition();
51+
cond1.Pattern.Value = "bcd";
52+
cond1.PatternType.Value = HighlightPatternType.Exact;
53+
cond1.HighlightOnlyMatch.Value = true;
54+
55+
highlightService.CreateCss(".wrapper");
56+
57+
var result = highlightService.CreateStyledLine("abcde");
58+
59+
// Expected behavior depends on implementation details of nesting,
60+
// but both classes should be present in some form.
61+
Assert.Contains("c0", result);
62+
Assert.Contains("c1", result);
63+
}
64+
65+
private static IServiceProvider CreateServiceProvider() {
66+
var services = new ServiceCollection();
67+
services.AddSingleton(Mock.Of<ILogger<SettingsStoreModel>>());
68+
services.AddSingleton(Mock.Of<ILogger<WorkspaceService>>());
69+
services.AddSingleton<WorkspaceService>();
70+
services.AddSingleton<SettingsStoreModel>();
71+
services.AddTransient<HighlightService>();
72+
services.AddScoped<HighlightConditionModel>(sp => new HighlightConditionModel(sp));
73+
services.AddSingleton<SettingsModel>();
74+
services.AddSingleton<HighlightSettingsModel>();
75+
services.AddSingleton<TextViewerSettingsModel>();
76+
services.AddSingleton<AdvancedSettingsModel>();
77+
services.AddScoped<HighlightRuleModel>();
78+
return services.BuildServiceProvider();
79+
}
80+
}

RemoteLogViewer.Core/Services/Viewer/HighlightService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,17 @@ public string CreateStyledLine(string content) {
135135

136136
var styledIndex = -1;
137137
List<PointStyle> applying = [];
138+
139+
var startsLookup = mergedWordStyles.ToLookup(x => x.Start);
140+
var endsLookup = mergedWordStyles.ToLookup(x => x.End + 1);
141+
138142
// スタイルの付け替えが発生する可能性のあるindexを列挙
139143
foreach (var index in mergedWordStyles.SelectMany(x => new int[] { x.Start, x.End + 1 }).OrderBy(x => x).Distinct()) {
140144
_ = sb.Append(Escape(content[(styledIndex + 1)..index]));
141145
styledIndex = index - 1;
142146

143-
var starts = mergedWordStyles.Where(x => x.Start == index).OrderByDescending(x => x.Priority).ToArray();
144-
var ends = mergedWordStyles.Where(x => x.End + 1 == index).OrderByDescending(x => x.Priority).ToArray();
147+
var starts = startsLookup[index].OrderByDescending(x => x.Priority).ToArray();
148+
var ends = endsLookup[index].OrderByDescending(x => x.Priority).ToArray();
145149

146150
// 今回終了
147151
foreach (var end in ends) {

0 commit comments

Comments
 (0)