Skip to content

Commit 2d498bb

Browse files
authored
pref: improve performance of shiki (#9)
1 parent a740f64 commit 2d498bb

14 files changed

Lines changed: 1106 additions & 185 deletions

File tree

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import top.howiehz.gradle.YamlPluginVersionSupport
2+
13
/*
24
* Halo Plugin Extra API - Build Configuration
35
*
@@ -115,7 +117,7 @@ tasks.register('processShikiResources', Copy) {
115117
// 为什么用 YamlPluginVersionSupport:比正则表达式更安全,保持 YAML 格式和注释
116118
def configurePluginYamlVersion() {
117119
def manifestFile = file('src/main/resources/plugin.yaml')
118-
return top.howiehz.gradle.YamlPluginVersionSupport.configurePluginYamlVersion(project, manifestFile)
120+
return YamlPluginVersionSupport.configurePluginYamlVersion(project, manifestFile)
119121
}
120122

121123
def jarLiteIndexTask

buildSrc/src/main/java/top/howiehz/gradle/MultiJarPluginComponentsIndexTask.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.LinkedHashSet;
99
import java.util.List;
1010
import java.util.Set;
11+
import lombok.Getter;
1112
import lombok.extern.slf4j.Slf4j;
1213
import org.gradle.api.DefaultTask;
1314
import org.gradle.api.file.ConfigurableFileCollection;
@@ -28,6 +29,7 @@
2829
* 支持多个 JAR 任务的插件组件索引生成任务
2930
* 解决 Halo 官方插件开发工具硬编码 "jar" 任务的限制
3031
*/
32+
@Getter
3133
@Slf4j
3234
@DisableCachingByDefault(because = "Not worth caching")
3335
public class MultiJarPluginComponentsIndexTask extends DefaultTask {
@@ -124,22 +126,6 @@ public void generate() throws IOException {
124126
log.info("Component index file: {}", outputPath.getAbsolutePath());
125127
}
126128

127-
public ConfigurableFileCollection getClassesDirs() {
128-
return classesDirs;
129-
}
130-
131-
public Property<String> getJarTaskName() {
132-
return jarTaskName;
133-
}
134-
135-
public ListProperty<String> getExcludePackages() {
136-
return excludePackages;
137-
}
138-
139-
public RegularFileProperty getOutputFile() {
140-
return outputFile;
141-
}
142-
143129
/**
144130
* Convert a "/"-based resource path to a "."-based fully qualified class name.
145131
*

src-js-bundle/shiki/src/main.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { codeToHtml, bundledLanguages, bundledThemes } from 'shiki/bundle/full'
22

3-
// 创建一个包装函数给 Javet 调用
3+
// 单个代码高亮 - 简单包装
44
async function highlightCode(code, options = {}) {
55
try {
66
const html = await codeToHtml(code, options)
@@ -10,6 +10,37 @@ async function highlightCode(code, options = {}) {
1010
}
1111
}
1212

13+
// 批量高亮 - 在一个引擎中处理多个代码块
14+
async function highlightCodeBatch(requests) {
15+
try {
16+
const results = {}
17+
18+
// 使用 Promise.all 在同一个引擎中并行处理
19+
const entries = Object.entries(requests)
20+
const promises = entries.map(async ([id, request]) => {
21+
try {
22+
const html = await codeToHtml(request.code, {
23+
lang: request.lang,
24+
theme: request.theme
25+
})
26+
return [id, html]
27+
} catch (error) {
28+
return [id, `Error: ${error.message}`]
29+
}
30+
})
31+
32+
const resolvedResults = await Promise.all(promises)
33+
34+
for (const [id, html] of resolvedResults) {
35+
results[id] = html
36+
}
37+
38+
return results
39+
} catch (error) {
40+
throw new Error('批量高亮失败: ' + error.message)
41+
}
42+
}
43+
1344
// 获取支持的语言列表
1445
function getSupportedLanguages() {
1546
return Object.keys(bundledLanguages)
@@ -22,8 +53,9 @@ function getSupportedThemes() {
2253

2354
// 暴露给 globalThis
2455
globalThis.highlightCode = highlightCode
56+
globalThis.highlightCodeBatch = highlightCodeBatch
2557
globalThis.getSupportedLanguages = getSupportedLanguages
2658
globalThis.getSupportedThemes = getSupportedThemes
2759

2860
// 导出
29-
export { highlightCode, getSupportedLanguages, getSupportedThemes }
61+
export { highlightCode, highlightCodeBatch, getSupportedLanguages, getSupportedThemes }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package top.howiehz.halo.plugin.extra.api.service.basic.config;
2+
3+
import lombok.Data;
4+
5+
/**
6+
* Configuration class for V8 engine pool.
7+
* V8 引擎池配置类。
8+
*/
9+
@Data
10+
public class JsEnginePoolConfig {
11+
12+
/**
13+
* Minimum pool size.
14+
* 最小池大小。
15+
* <p>Default is 1 to minimize memory usage while ensuring at least one engine is always
16+
* available.</p>
17+
* <p>默认为 1,以最小化内存使用,同时确保至少有一个引擎始终可用。</p>
18+
*/
19+
private int poolMinSize = 1;
20+
21+
/**
22+
* Maximum pool size.
23+
* 最大池大小。
24+
* <p>Default is 2.
25+
* Setting this too high may cause OutOfMemoryError.</p>
26+
* <p>默认为 2。
27+
* 设置过高可能导致内存溢出错误。</p>
28+
*/
29+
private int poolMaxSize = 2;
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package top.howiehz.halo.plugin.extra.api.service.basic.config;
2+
3+
import java.util.function.Supplier;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.stereotype.Component;
6+
import reactor.core.publisher.Mono;
7+
import run.halo.app.plugin.ReactiveSettingFetcher;
8+
9+
/**
10+
* Supplier for V8 engine pool configuration that fetches settings reactively.
11+
* V8 引擎池配置的供应器,以响应式方式获取设置。
12+
*/
13+
@Component
14+
@RequiredArgsConstructor
15+
public class JsEnginePoolConfigSupplier implements Supplier<Mono<JsEnginePoolConfig>> {
16+
private final ReactiveSettingFetcher fetcher;
17+
18+
/**
19+
* Get the V8 engine pool configuration from plugin settings.
20+
* 从插件设置中获取 V8 引擎池配置。
21+
*
22+
* @return Mono emitting the V8 engine pool configuration / 发出 V8 引擎池配置的 Mono
23+
*/
24+
@Override
25+
public Mono<JsEnginePoolConfig> get() {
26+
return fetcher.fetch("jsEnginePool", JsEnginePoolConfig.class);
27+
}
28+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package top.howiehz.halo.plugin.extra.api.service.js.post.render.shiki;
2+
3+
import java.time.Duration;
4+
import java.time.Instant;
5+
import java.util.concurrent.atomic.AtomicLong;
6+
import java.util.concurrent.atomic.LongAdder;
7+
import lombok.Getter;
8+
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.stereotype.Component;
10+
11+
/**
12+
* Metrics collector for Shiki cache performance.
13+
* Shiki 缓存性能指标收集器。
14+
*
15+
* <p>收集的指标包括:
16+
* <ul>
17+
* <li>缓存命中/未命中次数</li>
18+
* <li>渲染总耗时</li>
19+
* <li>渲染请求数</li>
20+
* <li>去重节省的请求数</li>
21+
* </ul>
22+
*/
23+
@Slf4j
24+
@Component
25+
public class ShikiCacheMetrics {
26+
27+
// 使用 LongAdder 提供更好的并发性能
28+
private final LongAdder cacheHits = new LongAdder();
29+
private final LongAdder cacheMisses = new LongAdder();
30+
private final LongAdder totalRenderTimeMs = new LongAdder();
31+
private final LongAdder renderCount = new LongAdder();
32+
private final LongAdder deduplicatedRequests = new LongAdder();
33+
private final AtomicLong lastResetTime = new AtomicLong(System.currentTimeMillis());
34+
35+
/**
36+
* Record a cache hit.
37+
* 记录缓存命中。
38+
*/
39+
public void recordCacheHit() {
40+
cacheHits.increment();
41+
}
42+
43+
/**
44+
* Record a cache miss.
45+
* 记录缓存未命中。
46+
*/
47+
public void recordCacheMiss() {
48+
cacheMisses.increment();
49+
}
50+
51+
/**
52+
* Record render time for a batch.
53+
* 记录批量渲染耗时。
54+
*
55+
* @param startTime render start time / 渲染开始时间
56+
*/
57+
public void recordRenderTime(Instant startTime) {
58+
long durationMs = Duration.between(startTime, Instant.now()).toMillis();
59+
totalRenderTimeMs.add(durationMs);
60+
renderCount.increment();
61+
}
62+
63+
/**
64+
* Record deduplicated requests count.
65+
* 记录去重节省的请求数。
66+
*
67+
* @param count number of duplicates removed / 去除的重复数
68+
*/
69+
public void recordDeduplication(int count) {
70+
deduplicatedRequests.add(count);
71+
}
72+
73+
/**
74+
* Get current metrics snapshot.
75+
* 获取当前指标快照。
76+
*
77+
* @return metrics snapshot / 指标快照
78+
*/
79+
public MetricsSnapshot getSnapshot() {
80+
long hits = cacheHits.sum();
81+
long misses = cacheMisses.sum();
82+
long total = hits + misses;
83+
84+
double hitRate = total > 0 ? (hits * 100.0 / total) : 0.0;
85+
double missRate = total > 0 ? (misses * 100.0 / total) : 0.0;
86+
87+
long renders = renderCount.sum();
88+
long totalTime = totalRenderTimeMs.sum();
89+
double avgRenderTime = renders > 0 ? (totalTime * 1.0 / renders) : 0.0;
90+
91+
long deduped = deduplicatedRequests.sum();
92+
long uptimeSeconds = (System.currentTimeMillis() - lastResetTime.get()) / 1000;
93+
94+
return new MetricsSnapshot(
95+
hits,
96+
misses,
97+
total,
98+
hitRate,
99+
missRate,
100+
renders,
101+
totalTime,
102+
avgRenderTime,
103+
deduped,
104+
uptimeSeconds
105+
);
106+
}
107+
108+
/**
109+
* Reset all metrics.
110+
* 重置所有指标。
111+
*/
112+
public void reset() {
113+
cacheHits.reset();
114+
cacheMisses.reset();
115+
totalRenderTimeMs.reset();
116+
renderCount.reset();
117+
deduplicatedRequests.reset();
118+
lastResetTime.set(System.currentTimeMillis());
119+
log.info("Shiki 缓存指标已重置");
120+
}
121+
122+
/**
123+
* Metrics snapshot record.
124+
* 指标快照记录。
125+
*/
126+
@Getter
127+
public static class MetricsSnapshot {
128+
private final long cacheHits;
129+
private final long cacheMisses;
130+
private final long totalRequests;
131+
private final double hitRatePercent;
132+
private final double missRatePercent;
133+
private final long renderBatchCount;
134+
private final long totalRenderTimeMs;
135+
private final double avgRenderTimeMs;
136+
private final long deduplicatedRequests;
137+
private final long uptimeSeconds;
138+
139+
public MetricsSnapshot(
140+
long cacheHits,
141+
long cacheMisses,
142+
long totalRequests,
143+
double hitRatePercent,
144+
double missRatePercent,
145+
long renderBatchCount,
146+
long totalRenderTimeMs,
147+
double avgRenderTimeMs,
148+
long deduplicatedRequests,
149+
long uptimeSeconds) {
150+
this.cacheHits = cacheHits;
151+
this.cacheMisses = cacheMisses;
152+
this.totalRequests = totalRequests;
153+
this.hitRatePercent = hitRatePercent;
154+
this.missRatePercent = missRatePercent;
155+
this.renderBatchCount = renderBatchCount;
156+
this.totalRenderTimeMs = totalRenderTimeMs;
157+
this.avgRenderTimeMs = avgRenderTimeMs;
158+
this.deduplicatedRequests = deduplicatedRequests;
159+
this.uptimeSeconds = uptimeSeconds;
160+
}
161+
162+
@Override
163+
public String toString() {
164+
return String.format(
165+
"ShikiCacheMetrics{缓存命中=%d, 未命中=%d, 总请求=%d, 命中率=%.2f%%, "
166+
+ "渲染批次=%d, 总耗时=%dms, 平均耗时=%.2fms, 去重节省=%d, 运行时间=%ds}",
167+
cacheHits, cacheMisses, totalRequests, hitRatePercent,
168+
renderBatchCount, totalRenderTimeMs, avgRenderTimeMs,
169+
deduplicatedRequests, uptimeSeconds
170+
);
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)