perf(优化性能): 优化原生资源传输与释放
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-17 20:31:26 +08:00
1 parent 253debb3f9
commit e261287baa
5 files changed
+188 -33

No files matched your search

+38 -8
View File
@@ -58,7 +58,11 @@ class NativeHandle {
async createWritable() {
const {writer} = await this.host.call("write.begin", {handle:this.nativeHandle});
return {
write: async data => this.host.call("write.data", {writer, data:await this.host.encode(data)}),
write: async data => {
const resources = new Set();
try { return await this.host.call("write.data", {writer, data:await this.host.encode(data, "", resources)}); }
finally { for (const file of resources) await this.host.call("release", {file}).catch(() => {}); }
},
close: () => this.host.call("write.commit", {writer}),
abort: () => this.host.call("write.abort", {writer}),
};
@@ -77,7 +81,7 @@ class NativeHost {
return this.bridge.call(`ofdgo.host.${name}`, value);
}
async encode(value, key = "") {
async encode(value, key = "", resources = null) {
if (value?.nativeFile) return {nativeFile:value.nativeFile, size:value.size, name:value.name};
if (value?.nativeHandle) return {nativeHandle:value.nativeHandle, name:value.name, kind:value.kind};
if (value instanceof Blob || value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
@@ -87,12 +91,19 @@ class NativeHost {
}
const response = await fetch("/__native/input", {method:"POST", body:value});
if (!response.ok) throw new Error("传递数据失败");
return response.json();
const file = await response.json();
resources?.add(file.nativeFile);
return file;
}
if (Array.isArray(value)) {
const results = await Promise.allSettled(value.map(item => this.encode(item, "", resources)));
const failure = results.find(item => item.status === "rejected");
if (failure) throw failure.reason;
return results.map(item => item.value);
}
if (Array.isArray(value)) return Promise.all(value.map(item => this.encode(item)));
if (value && typeof value === "object") {
const result = {};
for (const [key, item] of Object.entries(value)) result[key] = await this.encode(item, key);
for (const [key, item] of Object.entries(value)) result[key] = await this.encode(item, key, resources);
return result;
}
return value;
@@ -118,12 +129,23 @@ class NativeHost {
return value;
}
async release(value) {
if (value?.nativeFile && value.temporary) {
await this.call("release", {file:value.nativeFile}).catch(() => {});
return;
}
if (value && typeof value === "object") {
for (const item of Object.values(value)) await this.release(item);
}
}
worker() {
const host = this;
let closed = false;
let unsubscribe;
let pending = Promise.resolve();
const canceled = new Set();
const resources = new Set();
const emit = data => { if (!closed) worker.onmessage?.({data}); };
const ready = host.call("engine.open", {}).then(({engine}) => {
if (closed) { host.call("engine.close", {engine}).catch(() => {}); return engine; }
@@ -146,18 +168,26 @@ class NativeHost {
const engine = await ready;
if (closed) return;
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
const args = await host.encode(data.args);
const args = await host.encode(data.args, "", resources);
if (closed) return;
if (canceled.has(data.id)) throw Object.assign(new Error("操作已取消"), {name:"AbortError"});
const result = await host.call("engine.call", {engine, id:data.id, name:data.name, args});
emit({id:data.id, ...await host.decode(result, data.name)});
if (closed) { await host.release(result); return; }
const decoded = await host.decode(result, data.name);
if (closed) { await host.release(result); return; }
emit({id:data.id, ...decoded});
}).catch(error => emit({id:data.id, ok:false, error:error.message, code:error.code, canceled:error.name === "AbortError"})).finally(() => canceled.delete(data.id));
},
terminate() {
if (closed) return;
closed = true;
unsubscribe?.();
ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
const closing = ready.then(engine => host.call("engine.close", {engine})).catch(() => {});
pending.finally(async () => {
await closing;
for (const file of resources) await host.call("release", {file}).catch(() => {});
resources.clear();
});
},
};
ready.catch(() => {});
+1 -1
View File
@@ -8,7 +8,7 @@ require (
github.com/go-text/typesetting v0.3.5
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
github.com/xiaoqidun/guigo v0.0.0-20261017114758-498e989ad05f
github.com/xiaoqidun/guigo v0.0.0-20261017122947-f0c082d8bbcd
github.com/xiaoqidun/j2kgo v1.0.2
github.com/xiaoqidun/jbig2 v1.1.8
github.com/xiaoqidun/pdfgo v0.0.0-20261016003351-18348c283ac1
+2 -2
View File
@@ -58,8 +58,8 @@ github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/guigo v0.0.0-20261017114758-498e989ad05f h1:LReZHupfHnvaOvjGMdPShKDArLMOEF5Fwm8HjfOR9n4=
github.com/xiaoqidun/guigo v0.0.0-20261017114758-498e989ad05f/go.mod h1:QAHK2Sa/KvY6dO+6dtvVUeOXVQO4Zh8R5oSSU+av7kE=
github.com/xiaoqidun/guigo v0.0.0-20261017122947-f0c082d8bbcd h1:IzKVmVXKjcaClHFCou3MQmSuSq+dWIde7m4AwSmuCM0=
github.com/xiaoqidun/guigo v0.0.0-20261017122947-f0c082d8bbcd/go.mod h1:QAHK2Sa/KvY6dO+6dtvVUeOXVQO4Zh8R5oSSU+av7kE=
github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw=
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
+91 -11
View File
@@ -17,6 +17,7 @@
package webview
import (
"bytes"
"context"
"crypto/rand"
"encoding/json"
@@ -56,9 +57,10 @@ type Host struct {
pendingPaths []string
}
// resourceReader 保持资源独立的读取位置,文件生命周期由窗口管理
// resourceReader 保存资源读取位置及关闭操作
type resourceReader struct {
*io.SectionReader
close func() error
}
// Run 启动共用WebUI的原生入口
@@ -115,27 +117,105 @@ func Run(ctx context.Context, paths []string, dataDir string) error {
}
// Close 结束本次资源读取
func (r *resourceReader) Close() error { return nil }
// 返回: error 关闭错误
func (r *resourceReader) Close() error {
if r.close != nil {
return r.close()
}
return nil
}
// resource 将大文件直接交给宿主资源流
// resource 将静态资源及文件直接交给宿主资源流
// 入参: r 资源请求
// 返回: *guigo.Resource 资源响应, error 读取错误
func (h *Host) resource(r *http.Request) (*guigo.Resource, error) {
if r.Method != http.MethodGet || !strings.HasPrefix(r.URL.Path, "/__native/data/") || r.Header.Get("Range") != "" {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
return nil, nil
}
id := strings.TrimPrefix(r.URL.Path, "/__native/data/")
reader, size, err := h.files.resolve(id)
for _, name := range []string{"If-Match", "If-None-Match", "If-Modified-Since", "If-Unmodified-Since", "If-Range"} {
if r.Header.Get(name) != "" {
return nil, nil
}
}
var reader io.ReaderAt
var size int64
var close func() error
transferred := false
defer func() {
if !transferred && close != nil {
close()
}
}()
if strings.HasPrefix(r.URL.Path, "/__native/data/") {
var err error
reader, size, close, err = h.files.resource(strings.TrimPrefix(r.URL.Path, "/__native/data/"))
if err != nil {
return nil, nil
}
} else {
name := strings.TrimPrefix(r.URL.Path, "/")
if name == "" {
name = "index.html"
}
data, err := webuiassets.NativeFS.ReadFile(name)
if err != nil {
return nil, nil
}
reader, size = bytes.NewReader(data), int64(len(data))
}
header := make([]byte, min(size, 512))
_, err = reader.ReadAt(header, 0)
_, err := reader.ReadAt(header, 0)
if err != nil && err != io.EOF {
return nil, err
}
return &guigo.Resource{
Body: &resourceReader{io.NewSectionReader(reader, 0, size)},
Header: http.Header{"Content-Type": {http.DetectContentType(header)}, "Content-Length": {strconv.FormatInt(size, 10)}, "Cache-Control": {"no-store"}},
}, nil
contentType := mime.TypeByExtension(filepath.Ext(r.URL.Path))
if contentType == "" {
contentType = http.DetectContentType(header)
}
headers := http.Header{"Content-Type": {contentType}, "Content-Length": {strconv.FormatInt(size, 10)}, "Cache-Control": {"no-store"}, "Accept-Ranges": {"bytes"}}
start, length, status := int64(0), size, http.StatusOK
if value := r.Header.Get("Range"); value != "" && r.Method == http.MethodGet {
if !strings.HasPrefix(value, "bytes=") || strings.Contains(value, ",") {
return nil, nil
}
first, last, ok := strings.Cut(strings.TrimPrefix(value, "bytes="), "-")
if !ok {
return nil, nil
}
if first == "" {
count, err := strconv.ParseInt(last, 10, 64)
if err != nil || count <= 0 {
return nil, nil
}
length = min(count, size)
start = size - length
} else {
var err error
start, err = strconv.ParseInt(first, 10, 64)
if err != nil || start < 0 || start >= size {
return nil, nil
}
end := size - 1
if last != "" {
end, err = strconv.ParseInt(last, 10, 64)
if err != nil || end < start {
return nil, nil
}
}
length = min(end, size-1) - start + 1
}
if length == 0 {
return nil, nil
}
status = http.StatusPartialContent
headers.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, start+length-1, size))
headers.Set("Content-Length", strconv.FormatInt(length, 10))
}
if r.Method == http.MethodHead {
length = 0
}
transferred = true
return &guigo.Resource{Body: &resourceReader{SectionReader: io.NewSectionReader(reader, start, length), close: close}, StatusCode: status, Header: headers}, nil
}
// emit 发送当前窗口事件
+56 -11
View File
@@ -39,11 +39,13 @@ type fileEntry struct {
file *os.File
directory bool
owned bool
temporary bool
}
// fileStore 管理窗口范围内的文件授权和临时数据
type fileStore struct {
mu sync.Mutex
transfers sync.WaitGroup
entries map[string]*fileEntry
writers map[string]*fileWriter
temporary string
@@ -206,12 +208,43 @@ func (s *fileStore) bytes(data []byte) map[string]any {
return map[string]any{"nativeFile": id, "size": len(data), "name": "data", "url": "/__native/data/" + id, "temporary": true}
}
// resource 创建独立资源读取句柄
// 入参: id 文件标识
// 返回: io.ReaderAt 随机读取器, int64 文件大小, func() error 关闭函数, error 读取错误
func (s *fileStore) resource(id string) (io.ReaderAt, int64, func() error, error) {
s.mu.Lock()
defer s.mu.Unlock()
entry := s.entries[id]
if entry == nil || entry.directory {
return nil, 0, nil, os.ErrNotExist
}
if entry.data != nil {
return bytes.NewReader(entry.data), int64(len(entry.data)), nil, nil
}
file, err := openReadFile(entry.path)
if err != nil {
return nil, 0, nil, err
}
info, err := file.Stat()
if err != nil {
file.Close()
return nil, 0, nil, err
}
return file, info.Size(), file.Close, nil
}
// release 释放已传递完成的内存结果
func (s *fileStore) release(id string) {
s.mu.Lock()
defer s.mu.Unlock()
if entry := s.entries[id]; entry != nil && entry.data != nil {
if entry := s.entries[id]; entry != nil && (entry.data != nil || entry.temporary) {
delete(s.entries, id)
if entry.file != nil {
entry.file.Close()
}
if entry.temporary {
os.Remove(entry.path)
}
}
}
@@ -449,28 +482,39 @@ func (w *fileWriter) release() {
func (s *fileStore) serve(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
if r.URL.Path == "/__native/input" && r.Method == http.MethodPost {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
http.Error(w, os.ErrClosed.Error(), 500)
return
}
s.transfers.Add(1)
s.mu.Unlock()
defer s.transfers.Done()
file, err := temporaryFile(s.temporary, "input-")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
_, err = io.Copy(file, r.Body)
err = errors.Join(err, file.Close())
size, err := io.Copy(file, r.Body)
if err != nil {
file.Close()
os.Remove(file.Name())
http.Error(w, err.Error(), 500)
return
}
handle, err := s.add(file.Name(), false)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
value, err := s.file(handle["nativeHandle"].(string))
if err != nil {
http.Error(w, err.Error(), 500)
s.mu.Lock()
if s.closed {
s.mu.Unlock()
file.Close()
os.Remove(file.Name())
http.Error(w, os.ErrClosed.Error(), 500)
return
}
id := rand.Text()
s.entries[id] = &fileEntry{path: file.Name(), name: "data", file: file, temporary: true}
s.mu.Unlock()
value := map[string]any{"nativeFile": id, "size": size, "name": "data", "url": "/__native/data/" + id, "temporary": true}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(value)
return
@@ -508,6 +552,7 @@ func (s *fileStore) close() error {
s.writers = make(map[string]*fileWriter)
s.entries = make(map[string]*fileEntry)
s.mu.Unlock()
s.transfers.Wait()
var err error
for _, writer := range writers {
err = errors.Join(err, writer.Abort())