博客
Browser Native Data Engine:超大日志的浏览器端数据架构
Technical Share / Browser Native Data Architecture

Browser Native Data Engine

浏览器端超大数据存储、解析、查询、渲染与交互架构实践

CASE STUDY · bklog/web · retrieve-v3 · IndexedDB / WebWorker / NDJSON
主题:当一次查询进入浏览器的不是几十 KB,而是数百 MB 甚至 GB 级数据时,前端如何继续可用?
01 / Background

业务背景:日志的自由接入,把复杂度推到了浏览器

日志系统的价值在于帮助用户处理分布式系统中的海量日志:通过 Agent 采集、全文检索和场景化查询,让开发、运维和业务人员能够定位故障、分析链路和追溯行为。采集侧为了保留用户的自主接入能力,不要求所有日志统一格式,也不把单条日志和总量限制在一个理想范围内。

这意味着前端收到的数据可能是非结构化文本、嵌套 JSON、超长字段、带高亮标记的查询结果,甚至包含大整数。真实案例中,单条日志超过 1 MB、5 MB、10 MB,甚至达到 17 MB;一次查询 24~50 条数据,响应体达到 180~400 MB 并不罕见。

17 MB真实单条日志案例
417 MB40 条日志请求体案例
50 rows用户一次常见查询规模
content length
案例:40 条数据的 content-length 约 398 MB。问题从“渲染几行表格”变成“浏览器如何管理一段大型数据集”。

用户真正需要的并不是在第一帧看到所有字节,而是对结果集进行一组局部操作:查看某一行、搜索关键词、定位命中、展开 JSON、复制原文、查看高亮、按字段筛选。这组需求要求数据可寻址、可延迟解析、可重复读取,而不是要求所有 payload 同时进入 UI。

本分享的代码语境:蓝鲸日志平台前端 bklog/web 的默认检索页是 retrieve-v3(仅当 localStorage.retrieve_version === 'v1' 时回退旧版)。主查询链路为:Actions 组装参数 → WebWorker fetch /search/*?stream=true → NDJSON/JSON 解析 → IndexedDB 落盘 → Vuex 只保留 row_keys 与结果元数据 → 结果组件按 key 读取渲染投影。普通单索引、联合索引、场景检索三条后端分支必须与字段请求保持一致。
Problem framing / 复杂度进入浏览器的位置
接入自由非结构化 · 嵌套 JSON
大整数 · 超长字段
查询形态单索引 · 联合索引
场景检索 · 分页/流式
用户意图局部查看 · 高亮 · 复制
展开 · 筛选 · 定位
前端挑战主线程长任务 · 内存峰值
响应式放大 · 结果集所有权
02A / Incident Analysis

两个真实请求:从请求体到浏览器崩溃

这里不是五个独立场景,而是两次真实请求的两个观察面。40 条请求同时提供了 content-length 和 HTTP 面板数据;4 MB、5 MB 是这批结果中具有代表性的单行日志;24 条请求则展示了“行数减少,但响应仍然巨大”的另一种风险。

01 / Request请求发出size、begin、字段投影和检索条件决定进入浏览器的数据集。
02 / Responsecontent-length / HTTP40 条约 400 MB,24 条仍约 183 MB;响应体是风险入口。
03 / Row View4 MB / 5 MB 单行原始行、render row、JSON 展开、高亮和复制开始叠加成本。
04 / Failure长任务 / OOM输入无响应、页面掉帧、Worker 失败或标签页崩溃。
Request A

40 条数据:约 400 MB 的一次检索

这是同一请求的两个记录:content-length 从字节角度给出响应长度,HTTP 面板从 KB 角度展示网络统计。两个数字不是两个场景,而是同一次请求在不同观测面上的表现。

40 rows
417,583,636content-length / bytes
≈398.1 MiB二进制体积估算
417,814 KBHTTP 面板记录
≈10.0 MB平均每行(粗略)
40条数据 content-length
请求记录 1:content-length = 417,583,636 bytes
40条数据 HTTP 请求
请求记录 2:HTTP 面板显示 417,814 KB
如何理解:Network 下载完成并不代表页面安全。此后还要经历字符串解码、JSON 解析、字段映射、highlight overlay、渲染对象和表格更新。若这些步骤在主线程全量执行,约 400 MB 的响应会被放大成更高的瞬时内存峰值。
Request 40 rows417,583,636 bytesJSON / row viewLong Task / OOM
Request B

24 条数据:约 183 MB 的“较小”请求

24 条看起来比 40 条轻,但 HTTP 面板仍显示 187,344 KB。这个请求说明:风险不由行数单独决定,少量超长日志依然可以让单次检索进入浏览器高压区。

数据规模说明:这里的 187,344 KB 只用于展示这次 24 条数据请求的返回体大小。24 条并不意味着数据量小:平均每行约 7.6 MB,且实际分布可能包含 4~5 MB 甚至更大的长尾日志。响应返回之后,还需要经历 Blob/文本读取、BigNumber 解析、复杂 Object 构造、JSON string 格式化、分词、高亮、时间处理、别名映射和 Vue 2 响应式处理;这些后处理会继续增加内存占用,最终风险取决于数据处理链,而不只是 Network 面板上的返回体大小。
24 rows
187,344 KBHTTP 面板记录
≈182.95 MiB二进制体积估算
24返回行数
≈7.6 MB平均每行(粗略)
24条数据 HTTP 请求
请求记录:24 条数据,HTTP 面板显示 187,344 KB
如何理解:24 条数据的关键问题不是行数,而是返回体仍达到约 183 MB。数据进入浏览器后,即使只展示其中几行,后续的解析、对象展开、字段派生、分词、高亮、JSON 格式化和响应式处理仍可能制造大量中间对象;因此需要重点观察 Response 之后的处理链和内存峰值。
Request 24 rows187,344 KB全量解析与对象派生内存峰值上升

单行视图:两个请求中的长尾数据

4 MB 和 5 MB 单行日志不是额外的第三、第四个请求,而是上述请求返回结果中的代表性 row。它们解释了为什么“平均每行大小”会掩盖真实问题:一次请求可能由少量超长行贡献主要的解析和渲染峰值。

4MB单条日志
代表性 row:4 MB 单条日志
5MB单条日志
代表性 row:5 MB 单条日志
行视图的风险:用户点击展开、复制或查看 JSON 时,如果系统先构造完整编辑器模型,局部交互就会变成整行深层解析。Native Data Engine 应使用 rowKey 定位原始 payload,优先读取 renderMeta 和字段投影;复制原文不需要先构造完整可编辑 DOM。

两个请求的共同故障链

无论是 40 条还是 24 条,问题都不是某个单独数字,而是数据从网络进入浏览器之后连续穿过了多个放大点:

网络响应
入口
文本 / 解码
副本增加
JSON 对象
结构放大
row view + render
叠加副本
编辑器 / DOM
峰值风险
浏览器错误并不是从 Network 面板开始的。 数据请求只是风险进入浏览器的入口;真正的崩溃路径通常是:响应落地 → 整包解析 → 多份对象复制 → 行级格式化/高亮 → 编辑器或 DOM 构造 → 主线程长任务与内存峰值。

日志场景复杂度:Response 之后才是真正的处理链

请求返回的 ROW LIST 不是一组简单的字符串数组。每个 row 通常是一个 Object,字段类型混合存在:Value 可能是 JSON string、Text、Object、Number 或 Boolean;Object 的 KEY 可能超过 1000 个,嵌套深度实际可能达到 10 层以上。于是,Response 到达以后,浏览器面对的不是一次展示,而是一组连续的数据加工任务。

Row processing pipeline / 一条 Row 的完整处理链
1. Raw Row ObjectJSON string · Text · Object · Number · Boolean
1000+ keys · 10+ nested levels
2. Normalize大数归一化、字段类型判断、别名映射、原始值保留
3. Text Processing文本分词、token 构造、用户点击粒度、分词结果缓存
4. Highlight检索高亮标记、命中字段映射、overlay 与展示值合并
5. JSON ViewJSON string 识别、格式化、递归展开、层级节点生成
6. Time / Aliastimestamp → localString;dateNano 处理;别名字段映射
7. Vue 2 ReactivityObserver、Dep、Watcher、computed、watch、组件更新
8. View表格、虚拟列表、展开面板、编辑器、Tooltip、点击交互
处理能力业务要求新增内存 / CPU 成本
复杂字段解析兼容 JSON string、Text、Object、Number、Boolean,以及 1000+ KEY、10+ 层级对象节点、属性表、递归调用栈、字符串与引用增加
分词交互Text 分词,并支持用户按 token 点击token 数组、边界信息、点击索引、分词结果缓存
检索高亮命中内容可见,支持字段级高亮mark 片段、highlight overlay、原始值与展示值并存
JSON 解析JSON string 可格式化、递归展开再次 parse、格式化字符串、树节点和展开状态
时间处理timestamp 转 localString,dateNano 保留精度日期对象、格式化字符串、大数/纳秒转换中间值
别名映射原始字段与展示字段可用别名访问字段索引、映射表、原始 key 与 alias key 的重复引用
Vue 2 响应式Row 能被组件、watch、computed 持续观察Observer/Dep/Watcher、依赖收集、派生值和更新队列
复杂度结论:如果上述工作都在 Response 返回后、主线程、全量 ROW LIST 上同步完成,那么内存峰值不是“响应体 + 一点渲染开销”,而是 Raw Object、BigNumber、分词 token、高亮 overlay、格式化 JSON、时间派生值、别名映射和 Vue 2 响应式对象的叠加。Vue 的 watch/computed 并不会减少数据,只会为持续更新建立更多引用和依赖关系;在深层大对象上,内存与 CPU 成本可能出现非线性膨胀。

交互实验:改变“平均单行大小”,观察风险阶段

10 MB× 40 行模拟
1000个 / row
10
60%JSON string / Object / Array
响应体
解析对象
渲染峰值

当前规模下,整包 JSON 和 UI 副本仍有明显峰值;应优先采用流式摄取与按行持久化。

ResponseBlob数据完整落地
Blob → textdecode / copy再生成大字符串
BigNumber.parse精度保护整包建立对象
Row mappinglist + origin渲染副本叠加
BrowserGC / OOM长任务或崩溃

旧版本的关键代价:为了避免普通 JSON.parse 对大整数造成精度丢失,响应从 JSON 处理改为 Blob,再读取文本并优先执行 BigNumber Parse。它解决了数据正确性,却把整包 Blob、文本字符串、BigNumber 解析对象和渲染对象同时推入内存,形成更高的峰值和更长的 CPU 占用。因此,Blob 不是内存优化方案;它只是改变了响应读取方式。真正的优化需要把 BigNumber 解析拆到 Worker,并进一步采用 NDJSON 的逐行解析与批量落库。

复杂 Row 的估算边界:“平均单行 10 MB → 解析对象 540 MB”不能作为固定公式。ROW LIST 的每个 row 是 Object,Value 可能是 JSON string、Text、Object、Number、Boolean;Object 可能有 1000+ 个 KEY,嵌套深度可超过 10 层。解析成本不仅由 payload 字节决定,还取决于对象节点数量、字符串复制、属性表、递归遍历、BigNumber 实例、JSON 序列化、分词与 renderMeta。下面的交互实验因此采用“区间风险模型”,不是声称精确预测浏览器堆内存。
10 MB 是网络/文本 payload 的量级;解析对象的实际占用应通过真实样本的 heap snapshot、Worker timings 和字段分布测量。对象越深、KEY 越多、复合值比例越高,解析 CPU 和瞬时内存越容易出现非线性增长。
阶段如果继续沿用全量模型Native Data Engine 的对应策略
Request一次请求 size 过大,首屏等待不可控分页、stream=true、明确 begin/size
Responseresponse.text() 让大字符串整包落地ReadableStream 按字节读取,NDJSON 逐行解析
Row vieworigin、render、highlight、editor 多份复制rowKey + Metadata/Payload/RenderMeta 分离
Browser error主线程长任务、GC、OOM、页面崩溃Worker、batch write、24 MB 热缓存、TTL/GC、降级
与后续架构的对应:Request/Response 阶段对应 Worker 流式摄取与 Content-Type 分流;Row View 对应 origin_data / renderOverlay / renderMeta 分离;Browser error 对应主线程只持有 row_keys、热缓存上限、TTL/GC 与 IndexedDB 降级。详见 03~05 章。
旧全量模型 vs 当前 retrieve-v3 对同一故障链的切点
旧路径(放大)response.text → 整包 BigNumber.parse → Vuex 全量 rows → 主线程格式化/高亮/响应式
现路径(解耦)Worker fetch 流式解析 → IDB RowEntity → Vuex row_keys → 可见窗口按 key 投影
02 / Failure Mechanism

为什么 JSON.parse 会让浏览器“卡住”,甚至崩溃

“JSON.parse 很快”只在小对象上成立。对本场景,JSON.parse 只是把约 200 MB 的网络响应转换成 Object Tree,并不是最终内存。后续还会发生别名映射、JSON string 二次解析、文本分词、高亮、时间格式化、Vue 2 响应式转换以及 Watch / Computed 依赖收集。真正需要回答的问题是:同一份数据在这些阶段是否仍然被引用,以及每个阶段增加了多少对象、字符串和索引。

估算口径:下方数据是基于 V8、JSON.parse、Vue 2 Object.defineProperty 和当前业务处理链的工程估算,不是崩溃现场的 Heap Snapshot。不同浏览器版本、字段分布、嵌套深度、字符串编码和响应式覆盖范围会改变结果;最终应使用 Chrome DevTools Memory、Performance 和 Heap Snapshot 校准。

解析链路实验:同一份数据的成本如何叠加

动画只用于说明解析过程:数据越大,单次同步任务越长;主线程在任务结束前无法响应输入和绘制。

网络字符串
0 MB
解析对象
0 MB
渲染副本
0 MB
主线程响应
可响应
状态:等待运行
复杂 Row 处理后的内存叠加(示意)0 MB
Blob / 网络缓冲
0 MB
文本 / Blob → text
0 MB
BigNumber 对象
0 MB
Row Object / 深层节点
0 MB
分词 / 高亮 / JSON
0 MB
Vue 2 Observer / Watcher
0 MB

这是“同时存活对象”的示意,不是浏览器 heap 的精确测量。复杂 Row 可能同时保留原始字节、文本、对象树、派生展示值和响应式依赖;每一层都会增加峰值,且 GC 不一定能立即回收仍被引用的对象。

const text = await response.text();  // 整包落地
const rows = JSON.parse(text);        // 同步阻塞
const view = rows.map(formatRow);     // 再复制/格式化
render(view);                         // 触发布局与绘制

2.1 分阶段估算:200 MB Response 如何走向 2.2~3.6 GB

场景假设:ROW LIST 是 Array<Object>;单行可能有 1000+ Key、10+ 层嵌套;Value 混合 JSON string、Text、Object、Number、Boolean;后处理包含 Alias、Tokenizer、Highlight、JSON Format、Timestamp Format,并且全量进入 Vue 2 响应式系统。

阶段新增或保留的数据结构200 MB 场景的累计内存估算为什么会增长
Network BufferByte[] / 网络缓冲200 MB响应进入浏览器的起点
response.text()UTF-16 String380~420 MB整包文本副本;编码和解码造成额外占用
JSON.parse()Object Tree700~1100 MB属性、引用、字符串、嵌套节点和大数对象被创建
Alias MappingNew Object / 字段索引850~1300 MB原始 key 与 alias key 在映射期间同时存在
JSON String Parse嵌套 Object / Array1000~1700 MB字段内 JSON string 二次对象化、格式化树节点
TokenizerToken Object / 索引1200~2000 MB文本变成可点击 token,边界、类型和引用被保存
HighlightHighlight Metadata / Overlay1300~2200 MB原始值、命中值和展示值并存
Timestamp FormatLocal String / Nano String1350~2250 MB日期对象、格式化字符串和纳秒中间值
Vue2 ObserverGetter / Setter / Dep1800~3000 MB大量属性进入 Object.defineProperty 响应式系统
Watch / ComputedWatcher / Dependency Graph2200~3600 MB仍被引用的依赖图不能被 GC 回收
200 MBResponse 起点
2.2~3.6 GB全链路工程估算
11~18×相对 Response 放大

2.2 线性 Response,不代表线性内存

ResponseJSON Parse 典型区间Vue Reactive 典型区间最终内存估算
50 MB150~250 MB400~700 MB0.8~1.2 GB
100 MB300~500 MB800 MB~1.3 GB1.5~2.3 GB
200 MB700 MB~1.2 GB1.2~2 GB2.2~3.6 GB
400 MB1.5~2.5 GB2~4 GB4.4~7.2 GB

这些区间用于解释内存膨胀趋势,不应替代实际监控数据。相同 Response 在字段数、对象层级、JSON string 比例、token 数量和 Vue 响应式范围不同的情况下,可能落在不同区间。

2.3 内存放大:响应体不是最终内存

如果响应体是 200 MB,浏览器至少可能同时保留 UTF-16 字符串、解析后的对象、用于展示的投影对象,以及组件/编辑器内部的数据结构。真实占用取决于引擎、字段类型和复制路径,不能简单用“响应体大小”估算;但可以确定的是,一次性全量模型会放大峰值。

2.4 长任务:主线程是单线程调度器

浏览器主线程需要同时处理 JavaScript、样式计算、Layout、Paint、输入和框架更新。当 JSON.parse、深层遍历或大规模格式化形成一个长任务时,事件循环无法及时处理用户输入,表现为滚动掉帧、点击无响应、标签页假死。若内存分配持续失败,最终可能是页面崩溃,而非一个可捕获的业务异常。

2.5 DOM 与编辑器:VirtualList 不能替代数据架构

VirtualList 解决的是“当前 DOM 节点数量”,不能解决全量数据已经被解析、复制和保存在内存中的问题。Monaco、CodeMirror 等编辑器还会建立 token、行索引和装饰模型;把 17 MB 文本交给它们,成本会再次增加。

问题直接影响架构层面的根因
全量 JSON.parse主线程长任务解析边界与 UI 边界耦合
字符串/对象复制GC 频繁、峰值内存高数据没有引用和投影模型
全量 DOM/编辑器掉帧、布局成本高渲染层接管了数据集
搜索遍历 payload每次搜索都慢没有稳定的 queryKey、rowKey 和可查询元数据
旧请求晚返回结果乱序污染缺少请求身份、取消和版本判断
结论:不能把“解析导致崩溃”归因于某一个 API。需要同时控制解析粒度、执行线程、存储位置、内存上限、渲染窗口和请求生命周期。
导向后续章节:失效机理说明“为什么不能整包进 Vuex”。retrieve-v3 的应对不是取消 JSON/BigNumber,而是改变执行位置与所有权:解析与落盘进 Worker/IndexedDB;Vuex 只保留控制面状态;渲染只消费可见窗口的 projection。演进过程见 03,平台职责见 04,实体与读写路径见 05。
03 / Architecture Evolution

从问题阶段到 Browser Native Data Engine

这不是“先做一个页面,最后加一个缓存”的线性升级,而是一轮由数据规模驱动的探索:先确认问题来自哪里,再组合能够延长系统可用区间的方案,最后重新定义浏览器中数据的所有权、解析边界和生命周期。

STAGE 01

Entire Loading:问题阶段

API → JSON → Vue Store → Render

  • 响应完整落地,整包解析
  • ROW LIST 全量进入 Store
  • 字段格式化、分词、高亮、JSON 展开和时间处理同步发生
  • Vue 2 对大对象建立 Observer、Watcher 和 Computed 依赖
结论:小数据简单有效,大数据会把网络问题转化为主线程长任务、GC 压力和 OOM。
探索:先降低“立即进入视图”的压力
STAGE 02

组合探索:Slice + Lazy + Incremental

这不是三个孤立阶段,而是一组逐步组合的缓解策略。

String Slice只展示截断文本,降低首屏 DOM 与编辑器压力。
Lazy Loading展开、复制、查看详情时再请求,减少不必要的处理。
Incremental Loading滚动或分页时继续请求、解析、渲染,改善首屏响应。
有效区间KB 级、少量 MB:仍可顺畅运行;部分中等数据:体验有所改善。失效边界10 MB+ 单行、复杂嵌套、1000+ Key、JSON string 和全量 Vue Store:只减少可见内容,无法阻止数据在后台继续对象化和复制。
继续追问:谁解析?谁持有?何时释放?
STAGE 03

Browser Native Data Engine:架构阶段

从“优化渲染”转向“管理数据生命周期”。

解析边界Worker 内 ReadableStream / NDJSON(JSON envelope fallback);BigNumber 与行投影不在主线程全量执行。
存储边界Vuex 只保留 query state 与 row_keys;完整行以 queryKey:seq 落入 IndexedDB。
视图边界结果组件按可见 key 读取 renderMeta/overlay;复制/上下文再取 origin。
同步边界RequestPool、searchQueue、requestId、stale 清理、active query 心跳、TTL/GC 与 IDB 降级。

探索一:如何控制解析膨胀

第一步不是“把 JSON.parse 换成另一个 API”,而是拆开整包解析的连续成本。响应按字节读取,判断真实 Content-Type;流式响应按 meta / row / done 处理,普通 JSON envelope 保留 fallback(ndjson-stream.ts / Worker ingest)。每个 row 在 Worker 内生成 origin、renderOverlay 与 renderMeta,再由 StreamWriter 批量落库,避免主线程同时持有整包字符串、对象树和 UI 副本。

探索二:如何让数据离开内存而不是离开能力

如果把所有能力都建立在 Vuex 的完整 rows 上,搜索、复制和展开都必须持有大对象。新的所有权模型是:Vuex 持有查询状态、row_keys、total 和 loading;retrieve-row.repository 持有可恢复 Row Entity;retrieve-row-cache 保留约 24MB 热数据;renderMeta/overlay 保存截断与高亮派生信息。数据可从内存淘汰,能力仍通过 key 重新读取。

探索三:如何解决 Vue 依赖放大

Vue 2 的响应式系统适合业务状态,不适合承载数百 MB 的深层数据集。将大 payload 从响应式对象中隔离出来,只把轻量的 query state、row_keys 和交互状态交给 Vue;可见行通过异步读取得到非响应式投影,避免对完整 ROW LIST 建立 watch/computed 依赖图。

探索四:如何避免主进程阻塞

当解析、BigNumber、字段遍历和批量写入全部发生在主线程时,事件循环没有机会处理输入和绘制。retrieve-search-worker.service 串行调度,Worker 负责 fetch、解析、取消和 IndexedDB 写入;主线程只接收 meta、首行 progress、完整 row_keys、timings 和错误。Worker 是解析边界与 UI 边界的隔离层,不是附加优化。

最终分层架构:把探索结果固化为职责

Browser Native Data Engine / layered architecture
Application Layerretrieve-hub · retrieve-v3 · scene · origin / grep / clustering / chart用户意图与页面编排 · use-app-init
↓ query state / tab / visible intent
Interaction EngineSearch · Highlight · Token Click · Copy · Locate · Expand · Context以 rowKey 定位,不持有完整行数组
↓ visible keys / projection request
Rendering EngineVirtual Window · Chunk Render · Diff · Recycle · Editor Boundary优先 renderMeta / overlay · 展开再读 origin
↓ row_keys · result meta · pagination
Query Enginerow_query_key · rowKey · field_scope · Projection · replace/append · Stale Check普通 / 联合 / 场景分支对齐 · Actions 编排
Storage EngineRepository · IndexedDB · Hot Cache(~24MB) · TTL · GC · RenderMetaretrieveRows · fieldMetas · activeQueries
Synchronization EngineRequestPool · searchQueue · Timeout · requestId · Health · Fallback取消 / stale 清理 / IDB→volatile 降级
↕ Worker boundary · control msgs only (meta / progress / row_keys)
Ingest Runtime (WebWorker)fetch(?stream=true) · NDJSON / JSON envelope · JSONBigNumber · StreamWriter大 payload 不回主线程 · 10 rows / 8MB 批量落盘
↕ browser primitives
Browser Native PrimitivesWeb Worker · ReadableStream · AbortController · IndexedDB / Dexie · TextDecoder
查询与视图存储与数据同步 / Worker / 故障边界
当前实现映射:Application ≈ retrieve-v3;Query/Sync ≈ retrieve-search-actions + Worker Service;Storage ≈ Repository / RowCache / Dexie;Primitives ≈ Worker · ReadableStream · AbortController · IndexedDB。主查询不走 Axios 完整回包写入 Vuex,而是 Worker fetch 旁路(详见 05 / 06)。
APINDJSON / JSON
Workerfetch + parse
Repositorybatch write
Querykeys + projection
UIvisible window
04 / Browser Native Data Platform

从 Data Engine 到 Browser Native Data Platform

这里的 Native 不是指浏览器内置了某个数据库产品,而是指利用浏览器原生能力构造一套数据处理运行时:ReadableStream、Web Worker、IndexedDB、AbortController、事件循环和虚拟化渲染共同成为数据引擎的基础设施。

但这些基础设施只有在“读取之后立即预处理、预处理结果可复用、原始数据与展示数据分离”的前提下,才能真正降低内存压力。ReadableStream 获取到数据后,不直接把完整 ROW LIST 交给 View,而是在 Worker 内完成面向展示和交互的轻量预处理。

Runtime preprocessing / 读取后的数据处理
01 · TokenizeText 字段提前分词,生成可直接消费的 token 与点击边界;不在每次渲染时重复分词。
02 · Projection & Slice按字段和展示模式生成投影:前 1000 字符、16 KB 字段截取、32 KB 字段截取;完整值保留为按需读取的原始 payload。
03 · Alias Mapping建立原始字段与展示别名的映射索引,避免为每个别名复制一份完整 Row。
04 · Mark Overlay命中数据与原始数据分离保存;mark 只作为展示 overlay,复制时读取原始值并移除 mark 标签。
05 · Mode-aware Render紧凑模式默认渲染 1000 字符,点击更多展示 16 KB,全量单独展示;非紧凑模式默认展示 16 KB,点击展开渲染 32 KB,全量仍走独立查看。

它与普通前端页面的区别是:系统管理的不是一次 HTTP Response,而是一个可持续访问的 DataSet。页面拥有查询状态和用户意图;Platform 负责 DataSet、Entity Identity、Component、Projection、生命周期和存储 Provider。UI 通过 EntityId 和 Projection 获取结果,永远不直接依赖底层 Storage。

Browser Native Data Platform / domain architecture
DataSet-centric · Entity-centric · Projection-driven能力域视图 · 与 retrieve-v3 落地映射;虚线为可选/演进能力
Application Domain日志检索 · 结果表格 · JSON Viewer · 趋势 / Grep / 聚类
↓ Commands / Events
Interaction & Command BusSearch · Expand · Copy · Locate · Highlight · Export · Tab Switch
Projection EngineGrid · JSON Detail · Truncate / Token · Highlight Overlay
Query / Search Engine三分支字段+search · Filter 条件组装 · Pagination · row_keys 句柄全文检索仍在服务端
Lifecycle & Memory EnginequeryKey 身份 · Hot Cache · TTL · GC · Active Heartbeat · Health
↓ Entity operations / projection requests
Entity Manager管理 DataSet(一次查询结果集)、EntityId(rowKey)、Component、版本与可恢复生命周期
Row Entitykey = queryKey:seq
顺序、字节、expireAt
Origin Payloadrow / origin_data
复制与上下文
Field Metadatafield_scope 持久化
类型 / 别名 / 列信息
Render ComponentsrenderMeta · overlay
截断 / 高亮 / 投影
↓ Addressable Entity Store
Memory Provider热数据窗口 ~24MB LRU
IndexedDB Providerbklog-web-storage 持久化
OPFS / Snapshot大对象与快照演进
Remote Cache可选恢复层演进
输入边界:Network Response(含 Worker 流式摄取)只是 DataSet 的输入事件。进入 Platform 后,UI 不持有大对象,只持有 EntityId(rowKey)与 Projection;组件协作围绕 Entity + Component(origin / renderMeta / overlay)展开。IndexedDB 负责可寻址持久化,不是客户端搜索引擎。
Storage Engine

统一保存检索行、字段元数据、活跃查询和性能记录。IndexedDB(bklog-web-storage)是持久化仓库,Memory 是约 24MB 热缓存;Repository 提供 replace/append、bulkGet、按 queryKey 读取与 GC。
retrieve-row.repository.ts · db.ts · retrieve-row-cache.service.ts · retrieve-field-cache.service.ts

Query Engine

不直接返回大对象,而是建立 row_query_keyrowKey=queryKey:seq。字段请求与主查询按普通 / 联合 / 场景分支对齐;字段规范化写入 field_scope 后才允许主查询。
retrieve-search-actions.js · services/retrieve.ts

Rendering Engine

只为可见区域读取渲染投影;首行 progress 用于列宽,完整 row_keys 在 done 后提交。origin tab 复用 retrieve-v2 结果面板,但数据访问已改为按 key。
retrieve-v3/search-result · retrieve-v2/search-result-panel · renderMeta

Interaction Engine

高亮、复制、JSON 展开、定位都以 rowKey 定位实体:展示走 overlay/renderMeta,复制走 origin。Grep / 聚类 / 图表等 tab 复用查询状态,但不直接持有完整首屏行数组。

Synchronization Engine

RequestPool 取消旧字段/查询;Worker searchQueue 串行;requestId、pageInstanceId、queryKey、startSeq 判定 stale。activeRetrieveQueries 心跳保护 GC;IDB 失败降级 volatile memory。
retrieve-search-worker.service.ts · storageHealth

与 Platform 图的关系:上方 domain architecture 描述能力分层;下方五个 Engine 对应 retrieve-v3 落地模块。Network Response 只是 DataSet 输入事件;进入平台后,能力围绕 EntityId(rowKey)、Component(renderMeta/overlay)和 Projection 协作。
05 / Data Layer

数据层设计:从"响应对象"到"可寻址实体"

传统数据层将 Response 视为"一个大对象"——业务流程围绕 Object Tree 展开,组件通过 response.data.list 直接消费。当 Response 放大到数百 MB 时,这种依赖关系成为灾难:任何一个组件只要有 row 引用,整批数据就无法被 GC 回收。

可寻址实体模型的核心转变是:数据的身份不再由"它在对象树中的路径"决定,而是由 queryKey / rowKey 决定。Vuex 只保存查询状态、结果元数据和 row_keys;完整行由 Worker 解析后写入 IndexedDB,页面通过 key 按需读取渲染投影。

传统模型Response → Object Tree → Vuex rows
  • 组件直接持有 row 引用
  • 全量解析、全量响应式
  • GC 无法回收大对象
重构
可寻址实体模型Stream → RowEntity → rowKey → Projection
  • Vuex 只持有 row_keys
  • Worker 流式解析并落盘
  • 实体可持久化、可回收、可寻址

5.1 所有权边界:谁持有什么

与当前 retrieve-v3 实现一致:主线程不持有完整行数组;Worker 不渲染 UI;View 不直连 IndexedDB。边界按下图划分。

Ownership Boundary / 与 Vuex · Worker · IndexedDB 对齐
VuexindexItem · fieldInfo
loading · total · timings
row_keys / queryKey
Worker ServicesearchQueue 串行
cancel / timeout
progress → action
WebWorkerfetch · NDJSON/JSON
origin/data/renderMeta
batch write
IndexedDB + CacheretrieveRows
fieldMetas · activeQueries
24MB 热缓存 / 降级
UI dispatchActionsWorker.searchStreamIDB 落盘row_keys 回 VuexCache 按 key 读渲染行

5.2 写入路径 vs 读取路径

旧图把摄取与渲染揉在同一条“15 步点阵”里,消息方向不清晰。正确拆法是两条反向路径:写入让大对象离开主线程读取只按可见窗口取投影

WRITE PATH · 流式摄取落盘大 payload 不回到 Vuex
1
Actions 组装查询字段元数据就绪后生成 row_query_key、startSeq、writeMode意图 → 查询参数
2
Worker Service 调度searchStream 入队;RequestPool / AbortController 取消旧流参数 → postMessage
3
Worker fetchPOST /search/*?stream=true,按 Content-Type 选 NDJSON 或 JSON envelopeHTTP → 字节流
4
逐事件解析meta / row / done;行内保留 origin_data、data,并生成 renderMeta / overlay字节 → RowEntity
5
批量写入 IDBStreamWriter:10 行或 8MB 触发 bulkPut;key = queryKey:seqEntity → retrieveRows
6
轻量进度回传首行 progress(列宽)→ done 后完整 row_keys / total / timingsEntity → 控制消息
7
Actions 校验后 commit仅当前 requestId / queryKey 可写入 Vuex;stale 只清自身缓存控制消息 → Vuex 元数据
READ PATH · 按 key 渲染可见窗口才读实体
1
结果组件订阅 Vuex拿到 row_keys、loading、total,不持有完整 rowsVuex → 行键列表
2
可见窗口求 key 子集VirtualList / 表格只对当前视口 keys 发起读取row_keys → visible keys
3
RowCache 优先命中~24MB LRU 热缓存;大行可跳过热缓存直读 IDBkey → 内存热点
4
Repository bulkGet未命中则从 retrieveRows 取 entitykey → RowEntity
5
生成渲染投影优先 renderMeta / overlay;展开/复制再取 origin rowEntity → projection
6
View 局部更新表格行、高亮、JSON 展开、复制;交互仍以 rowKey 定位projection → DOM
7
分页 append同 queryKey,startSeq = 已有行数;首屏未完成时禁止抢占回到 WRITE PATH
进度策略(与实现一致):meta 到达即可更新 total/字段信息;首行写入后发一次 progress,用于尽早计算默认列;完整 row_keysdone 后回传,避免流式过程中频繁重排表格。

5.3 跨参与者消息序列(标准读写)

下表按“发送方 → 接收方”列出关键消息,对应 retrieve-search-actions → Worker Service → Worker → IndexedDB → Vuex → 结果组件。

阶段FromTo消息 / 数据形态
PrepareMain/ActionsField Cache字段元数据就绪 · normalize + field_scope 写入 retrieveFieldMetas
DispatchActionsWorker SvcsearchStream · url/body/queryKey/startSeq/writeMode/fieldMetadata
FetchWorkerSearch APIfetch stream=true · Traceparent + AbortController
StreamSearch APIWorkerNDJSON events · meta / row(origin_data,data) / done · JSON fallback
PersistWorkerIndexedDBbulkPut retrieveRows · key=queryKey:seq · replace|append
ProgressWorkerActionspostMessage · meta / first-row / timings(轻量,不含全量行)
DoneWorkerActions/Vuexrow_keys + total · 校验当前请求后 commit indexSetQueryResult
RenderViewRowCachegetByKeys(visible) · 热缓存未命中再 Repository.bulkGet
ProjectRepositoryViewrender row / overlay / origin · 按交互选择投影,不污染原始行
Data Morphology / 后端响应到渲染行的形态拆分
后端响应
  ├─ meta / 结果元数据 ──► Worker progress ──► Vuex indexSetQueryResult
  └─ row / origin_data + data
       └─► RetrieveRowRepository
            └─► IndexedDB retrieveRows
                 └─► row_keys ──► Vuex
                      └─► SearchResultPanel / LogRows
                           └─► retrieveRowCacheService 按 key 读取
                                └─► 渲染行 = origin + overlay + renderMeta

5.4 行实体结构:一份记录,三类载荷

实现上不是三套独立主键,而是 retrieveRows 中一条 RetrieveRowEntity(key = queryKey:seq)同时携带原始行与渲染派生信息;字段元数据则按 field_scope 单独持久化。

ROW ENTITY · retrieveRows

key = queryKey:seq

Worker 批量写入的最小可寻址单元。UI 永远通过该 key 访问,而不是持有对象引用。

row · origin_data(复制/上下文)
renderOverlay · 高亮 mark 叠加(不污染原文)
renderMeta · 字段投影、截断、分词边界等
seq / bytes / expireAt · 顺序、体积、TTL(默认 30min)
FIELD META · retrieveFieldMetas

key / scope = field_scope

查询前写入:规范化字段、字段树、别名索引。主查询必须等待字段元数据就绪。

raw / normalized fields
fieldTree · aliasIndex
供 Worker 投影与表格列头
QUERY STATE · Vuex

不存完整行

只保存可驱动 UI 的轻量状态与行键,作为结果集句柄。

row_query_key · row_keys
total · loading · timings
indexItem / indexFieldInfo
分离收益:首屏用 renderMeta/overlay 出表;复制/上下文读 origin;字段变更不影响已落盘行键。高亮信息不写回原始 payload,避免污染导出与复制。

5.5 IndexedDB Schema(bklog-web-storage)

以当前 Dexie schema 为准。行数据与字段元数据分表;活跃查询心跳保护 GC 不误删正在使用的结果集。

IndexedDB Tables

TableKeyIndexes用途
retrieveRowskey (queryKey:seq)queryKey · [queryKey+seq] · seq · expireAt行实体 + overlay/meta
retrieveFieldMetaskeyscope · updatedAt · expireAt字段规范化与别名
retrieveFieldWidthskeyscope · [scope+fieldName]列宽提示/用户宽度
activeRetrieveQueriesownerIdqueryKey · updatedAt · expireAt活跃查询心跳 / GC 保护
apiCacheskeyexpireAt · updatedAtAPI 元数据缓存(不含结果行)
performanceRecords++idsessionId · type · timestamp摄取/渲染耗时观测

复合索引 [queryKey+seq] 支持按查询分页读取;行 TTL 默认 30 分钟。

主线程 Memory / 降级

LayerCapacityEvictionAccess
Row Hot Cache~24 MBLRU(按字节估算)可见行渲染
Vuex Result Meta轻量新查询 / 卸载重置loading · row_keys · total
Active Query Heartbeat极小每分钟心跳 / 过期清理保护当前 queryKey
Volatile fallback页面内存刷新即失IndexedDB 不可用时

IDB 读写失败 → storageHealth 降级 → volatile memory,并提示刷新后不保留缓存。

5.6 写入策略:批量阈值 + replace/append + 竞态

STRATEGY A

批量化写入

StreamWriter 以 10 行或 8 MB 为默认批次(任一阈值达到即 flush)。每 5 行或每批后 setTimeout(0) 让出事件循环。

阈值10 rows or 8 MB 策略bulkPut → yield → 下一批
STRATEGY B

双模式:replace / append

首屏 replace:新 queryKey,删除该 key 旧行后写入。分页 append:同 queryKey,startSeq = 当前 row_keys.length。

replace首屏 · 释放上一查询行数据 append分页 · 禁止打断未完成的首屏流
STRATEGY C

请求身份与 stale

queryKey + requestId + pageInstanceId 共同判定。过期响应只清理自身查询键,不覆盖当前 Vuex。

取消RequestPool + AbortController 串行Worker searchQueue 避免并发写同一缓存
STRATEGY D

热缓存与 GC

热缓存约 24MB;活跃查询心跳保护;新查询、卸载、TTL 到期触发释放。IDB 失败则 volatile 降级。

上限~24 MB Hot Cache 降级IDB fail → volatile memory

5.7 数据流标识:各阶段可寻址身份

UI 不持有数据对象,只持有标识;通过标识访问 Repository / Cache。

阶段数据流标识承载者可寻址方式生命周期
字段准备field_scopeField Cache / IDBscope 主键空间/索引维度
查询发起row_query_keyVuex / Actions一次检索身份查询会话
流式摄取seqWorker StreamWriter行序号写入期间
持久化queryKey:seqIndexedDB retrieveRowsentity.keyTTL ≈ 30min
结果句柄row_keys[]Vuex indexSetQueryResult有序 key 列表当前结果集
热缓存rowKeyRowCacheServiceMap key / LRU字节上限内
渲染rowKey + projection结果组件render / origin / copy可见窗口
GCexpireAt + activeQueryRepository / Heartbeat排除活跃 queryKey后台回收
标识驱动原则:组件应持有 rowKey,通过 cache/repository 按需取 renderorigin。这样“数据不在内存”与“数据仍可访问”可以同时成立。

5.8 参与者职责隔离

Vuex / ActionsqueryState · row_keys
fieldInfo · cancel · stale check
Workerfetch · BigNumber · NDJSON
batchWrite · progress/done
Repository / Cachereplace · append · bulkGet
TTL · GC · health / volatile
Viewvisible keys · projection
expand · copy · locate

主线程不持有完整 Response 行数组;Worker 不负责 UI 渲染;Repository 不暴露 原始 IDB 连接给业务组件;View 不直接读取 IndexedDB。协作协议是 key + projection + progress 控制消息

约束边界:IndexedDB 不是搜索引擎,不支持全文检索和复杂聚合。客户端侧能力应限制在基于字段元数据的过滤、投影和按 key 读取;复杂检索仍由服务端 /search/* 完成。Entity 可寻址解决的是“可恢复、可引用、可回收”,不是“浏览器内任意查询”。
06 / Data Flow

一次检索请求的完整生命周期

完整生命周期包含两段:页面初始化(索引集与字段)与主查询流式摄取。前者走主线程 http.request;后者走 Worker fetch 旁路。二者都依赖 Vuex 中的空间/索引状态,但只有后者把大结果集实体化到 IndexedDB。

A. 页面初始化(use-app-init)
RetrieveHub → v3 解析 URL / 空间 / 索引模式 GET /search/index_set/ 字段分支 normalize + field_scope 缓存 origin tab 触发查询
普通字段GET /search/index_set/:id/fields/
联合字段unionSearch/unionMapping
场景字段POST /search/scene/fields/
场景空筛选跳过字段与主查询,展示空状态
B. 主查询流式生命周期 · 与 05 写入/读取路径对齐
1. Prepare字段元数据就绪;生成 row_query_key、可见字段、begin/size、writeMode
2. DispatchWorker service 串行 searchStream;URL 带 stream=true;取消旧请求
3. FetchWorker fetch + AbortController;补充 Traceparent
4. Detect按 Content-Type 选择 NDJSON 流或 JSON envelope fallback
5. Ingestmeta / row / done;构造 origin + overlay + renderMeta
6. Persist10 行或 8MB bulkPut → retrieveRows;key=queryKey:seq
7. Applyprogress/done 回主线程;校验 requestId 后 commit row_keys
8. Render结果组件按可见 row_keys → RowCache/IDB → 渲染投影
主查询 API 分支(字段与 search 必须同分支)
普通POST /search/index_set/:id/search/?stream=true
replace / append
联合POST /search/index_set/union_search/?stream=true
replace / append
场景POST /search/scene/search/?stream=true
replace / append
衍生能力趋势 / Terms / 上下文 / 实时
导出 / Grep / 聚类(独立请求)
控制面 vs 数据面
控制面(主线程)query 参数 · loading · total · row_keys · stale 判断 · 取消 · 菜单/空间状态
数据面(Worker + IDB)字节流解析 · RowEntity 落盘 · 热缓存 · TTL/GC / 降级

首屏与分页为什么使用不同写入模式

首屏使用 replace:新 row_query_key 建立后删除该查询旧行,并释放上一查询缓存。分页使用 append:沿用当前 queryKey,以已有 row_keys.length 作为 startSeq。若首屏流尚未结束,分页会被标记为 initial-search-loading 并忽略,避免 append 抢占 replace。

进度事件为什么只发送 meta 和首行

Worker 在 meta 到达时更新 total 等结果元数据;首行写入后发一次 progress,用于尽早计算默认列宽;完整 row_keysdone 后回传。主线程收到的是小型控制消息,而不是逐行大对象,避免流式过程中反复重排表格。

旧响应如何被丢弃

每次查询携带独立 queryKey、requestId、pageInstanceId。回调应用前检查是否仍为当前请求;不匹配则标记 stale,只清理自身查询键写入的缓存,不覆盖当前 Vuex 结果。分页取消属于控制流:保留已有行键和总数,不把正常竞态展示为错误。

卸载与回收

页面卸载时取消进行中的请求,释放当前查询的活跃标记与行缓存,并重置结果元数据、字段运行态。GC 结合 TTL(行默认约 30 分钟)与 activeRetrieveQueries 心跳,排除仍在使用的 queryKey。

07 / Implementation Details

项目内关键实现的对应关系

以下对应日志检索 V3 主链路中的职责划分。结果面板部分仍复用既有展示组件,但数据访问已改为按行键读取存储层,而不是持有完整结果数组。

功能模块承担职责设计要点
静态壳 / 启动首屏占位与全局预加载页面打开先展示加载壳;并行准备空间、用户与全局配置;应用挂载后淡出占位;菜单等非关键数据空闲补齐
入口分流新版 / 旧版检索选择默认进入 V3;仅显式兼容开关时回退旧版容器
检索页面编排初始化与状态门控解析 URL、空间与索引模式;拉取索引集与场景配置;字段就绪后再触发首屏查询;卸载时清理请求与缓存
检索状态编排字段与主查询协调普通 / 联合 / 场景三分支对齐;区分首屏替换与分页追加;用行键与请求身份丢弃过期响应
API 契约检索相关接口定义字段、查询、聚合、上下文、导出等;主查询实际由后台任务流式拉取,不把完整大结果直接塞进页面状态
后台任务调度主线程与摄取任务协调串行排队、任务复用、超时、取消,以及进度/完成消息回传
后台数据摄取请求、解析与落盘流式拉取、大数安全解析、NDJSON / JSON 兼容、批量写入持久层;大 payload 不回到主线程
流式解析响应拆解为数据事件按字节读取、逐行解析,批量让出事件循环;后端未流式时回退完整 envelope
行实体仓储可寻址实体存取与回收按查询键与序号生成稳定行键;支持替换/追加、批量读取、TTL 与 GC;保护活跃查询不被误删
缓存与健康管理热数据、字段缓存与降级热缓存设容量上限并近似 LRU;字段元数据按作用域持久化;持久层不可用时回退易失内存
结果展示按行键渲染与交互原始日志 / Grep / 聚类 / 图表等能力共享查询状态;只读取可见窗口投影,不持有完整首屏行数组

主查询旁路 vs 常规接口调用

常规接口调用预加载、菜单、索引集、字段、趋势等
走统一 HTTP 封装与服务定义
主查询旁路组装查询 URL / 参数后
在后台任务中流式拉取,不经完整回包写入页面状态

NDJSON 与普通 JSON 的兼容

const contentType = response.headers.get('content-type') || '';
if (shouldUseNDJSONStream(url, contentType)) {
  result = await ingestNDJSONStream(message, response, timings);
} else {
  // 后端未流式或中间层缓冲时,完整 envelope 仍可正常处理
  result = await ingestJsonEnvelopeRows(message, await parseJsonEnvelope(response), timings);
}

为什么使用大数安全解析

日志中的时间戳、ID 或业务序号可能超过 JavaScript 安全整数范围。后台摄取侧使用大数安全解析,避免普通 JSON.parse 把大整数静默舍入。这是数据正确性要求;性能优化来自“放到后台任务 + 流式/批量”,而不是去掉精度保护。

故障与降级边界

  • 后台任务不可用:直接报告能力不可用,不假装能完成大数据摄取。
  • 持久化存储不可用:提示兼容性问题并降级到有限易失内存;刷新后不保留。
  • 请求超时或任务错误:取消任务、清理等待队列,必要时重建任务运行时。
  • 存储写入失败:重置健康状态,保留当前页可用性,但不继续把无限数据塞进主线程。
  • 响应被取消:作为控制流处理,保留已成功缓存的结果,不把正常取消显示为无数据。
  • 字段与查询分支不一致:会导致投影/列错误;编排层必须按检索类型选择同一分支接口。
维护注意:修改响应结构时同时更新流式与完整 JSON 两条路径;修改字段结构时先更新规范化与字段作用域缓存;修改分页/取消时保留查询键、分页序号与过期响应判断;修改行展示时区分原始行、渲染行与高亮叠加。
08 / Trade-offs

方案收益、边界与工程权衡

传统模型Browser Native Data Engine(retrieve-v3)代价
整包 JSON.parse / Blob+BigNumberNDJSON 增量解析 + JSON envelope fallback服务端需支持流协议;客户端维护双路径
Vuex 持有完整 rowsVuex 持有 query state + row_keys组件改为异步按 key 读取
主线程处理全部工作Worker 摄取与落盘消息协议、生命周期、错误诊断更复杂
内存/LocalStorage 缓存IndexedDB Repository + TTL/GC + 热缓存schema、健康检测、浏览器兼容与降级
全量渲染可见窗口 + projection复制/展开需 key→entity 流程
单次请求无身份queryKey + requestId + searchQueue竞态与 stale 清理逻辑必须正确
一种查询路径普通 / 联合 / 场景三分支对齐字段与 search API 变更要同步三处
重要边界:IndexedDB 不是无限内存,也不是搜索引擎。它解决浏览器端可持续保存和按键读取;复杂全文检索、聚合仍应由服务端 /search/*aggs/* 完成。客户端侧能力以明确场景和预算为前提。
不该误解的点:1)V3 页面 ≠ 所有结果组件都迁到新目录(origin 仍复用 v2 panel);2)Worker fetch 旁路 ≠ 废弃 services 定义(URL/契约仍来自 retrieve 服务域);3)热缓存 24MB ≠ 结果集上限(完整集在 IDB,热缓存只加速可见窗口)。

如何验证这套架构真的有效

  1. 记录 fetch、stream、write、total 各阶段耗时(Worker timings),而不是只看接口耗时。
  2. 记录解析峰值、热缓存字节数、IndexedDB 写入批次、行数和 GC 清理量。
  3. 在 4 MB、5 MB、17 MB 单行和 24/40/50 行组合下测试首屏、滚动、搜索、展开、复制和取消。
  4. 用 Performance Long Task、内存快照和页面崩溃率验证主线程是否从大 payload 解耦。
  5. 覆盖慢网络、后端返回普通 JSON、Worker 失败、IndexedDB 禁用、连续快速查询、首屏未完成时分页、联合/场景分支。
  6. 卸载页面后确认活跃查询标记释放、请求取消,且无错误把取消展示为“无数据”。
09 / Roadmap

从超大日志承载走向可复用的浏览器数据基础设施

当前 retrieve-v3 已把“检索结果”从一次性响应提升为浏览器内可寻址数据集:流式摄取、Worker 解析、IndexedDB 实体、row_keys 查询、可见窗口渲染、竞态与降级边界。后续演进应围绕引擎能力,而不是单个页面组件。

Already landed / 已落地能力(本分享基线)
控制面Vuex 元数据 + row_keys
三分支字段/查询 · stale 控制
摄取面Worker fetch · NDJSON/JSON
JSONBigNumber · 批量落盘
存储面retrieveRows · fieldMetas
24MB 热缓存 · TTL/GC/降级
视图面按 key 投影渲染
origin 复用 v2 panel
Near-term → Mid-term → Long-term
Streaming Parser更细粒度 tokenizer、背压、暂停/恢复和错误定位;强化 JSON envelope 与 NDJSON 一致性。
Worker Engine任务优先级、可观测指标面板、大文本专用处理;必要时 Worker 池化。
Result Surface 统一逐步收敛 v2/v3 结果组件的数据访问协议,全部强制走 rowKey API。
Offline Replay利用已缓存 queryKey 结果集支持断网复盘与会话恢复。
Local Query在明确预算下对当前结果集做字段过滤/排序;全文仍以服务端为准。
Browser Native AI以 rowKey 为证据链,对 projection 做归纳;大模型可选远端。

Browser Data Engine 的本质,不是“把 IndexedDB 接进页面”,而是重新设计数据在浏览器中的生命周期:数据流式到达、在 Worker 中解析、以实体形式持久化、以 key 形式查询、以窗口形式渲染、以事件形式交互,并在每个环节设置容量和故障边界。

展望:从本地数据引擎到 Browser Native AI

当结果集以 Entity 形式进入 IndexedDB,浏览器就具备可持续访问的本地数据面。后续可以在不重复拉取完整 Response 的前提下,围绕当前 queryKey 做复盘、局部查询和智能交互——前提是答案必须绑定 rowKey、字段与时间范围。

Local Data → Query → AI / 浏览器端数据智能演进
Local Query Engine基于 queryKey、rowKey、字段元数据执行过滤、排序、分页(预算内)。
Semantic Index对选定字段生成轻量索引或 embedding,支持相似日志聚类。
LLM / Agent自然语言 → 结构化查询;解释结果、总结异常;证据必须可回溯到 rowKey。
Model RuntimeWebGPU / WASM / Worker 轻量模型;能力不足时回退远端。
Grounded Answer回答绑定 rowKey、字段、时间范围,避免脱离日志臆测。
Action Loop继续过滤、展开关联事件、生成时间线或保存查询视图。
可落地的 AI 形态:用户输入“找出这批日志中最可能导致超时的共同字段”,本地先按字段元数据筛选候选 keys,再把少量 projection 交给模型;模型只负责归纳,不需要把完整数百 MB Response 塞进上下文。
工程边界:浏览器端 LLM 需考虑模型体积、内存、隐私与可验证性。更现实的路线是“本地结构化查询 + 小上下文证据 + 必要时远端推理”,而不是把全部日志和大模型同时加载进页面。
演进目标:IndexedDB 保存事实,Query/Projection 产生证据,LLM 负责理解与编排;三者通过 rowKey、字段和时间范围连接,形成可复现的数据闭环。
基线验证结果(分享口径):按每页约 300 MB、连续请求 10 次估算,累计处理约 3,000 MB。完整结果主要由 IndexedDB 承载,主进程保留 query state、row_keys 与受限热缓存时,页面内存可稳定在约 300 MB 量级。这说明:累计处理量不再等价于主进程占用,关键在于是否避免把 Response、Row Object 与派生视图同时留在内存中。具体数字需用 Performance / Heap Snapshot 在目标环境校准。
10 / Evidence

实际证据:常规业务与极端连续大请求

证据分两档:95% 常规业务下,瞬时内存约 500+ MB、稳定后约 350 MB 左右;极端连续大请求下,四次合计传输约 1.05 GB,主线程仍可稳定在约 1,236 MB,而不是随累计下载线性堆到数 GB。

10.1 95% 业务:瞬时约 500+ MB,稳定约 350 MB 左右

常规检索以较小的流式响应为主(单次约百 KB~两百 KB 量级)。摄取与渲染过程中主线程会出现瞬时抬升,回落后稳定在约 350 MB 左右。

≈200 KB常规单次流式响应量级
≈521 MB瞬时主线程内存
≈370 MB稳定主线程内存(约 350 MB 左右)
常规业务流式请求网络面板
Network:常规 search/?stream=true,单次约 170~200 KB
常规业务瞬时内存约521MB
Memory:瞬时 Main ≈ 521 MB(500+ MB)
常规业务稳定内存约370MB
Memory:稳定 Main ≈ 370 MB(约 350 MB 左右)
常规业务解读:对绝大多数检索场景,内存峰值出现在流式解析与首屏投影阶段;落盘与可见窗口渲染完成后回落到约 350 MB 左右。这说明日常体验不是“每次查询都顶满 GB 级内存”,而是可控的瞬时抬升 + 稳定水位。

10.2 极端场景:四次大请求合计约 1.05 GB,内存约 1,236 MB

连续发起多次超大结果检索时,网络累计超过 1 GB;主线程 JS 堆稳定在约 1,236 MB,并未按累计下载量线性放大。

4连续大结果请求次数
≈1.05 GB四次请求合计传输量
≈1,236 MB主线程内存稳定水位
请求接口状态大小(HTTP 面板)
1search/?stream=true200305,112 KB
2search/?stream=true200231,363 KB
3search/?stream=true200257,177 KB
4search/?stream=true200257,024 KB
合计1,050,676 KB ≈ 1,026 MiB ≈ 1.05 GB
四次流式检索网络请求
Network:四次大结果请求(合计约 1.05 GB)
主线程内存约1235MB
Memory:Main ≈ 1,235 MB(约 1,236 MB)
两档证据对照
95% 常规业务单次响应百 KB 级;瞬时约 500+ MB,稳定约 350 MB 左右。日常可用性由稳定水位决定。
极端连续大请求累计传输约 1.05 GB;主线程约 1,236 MB。完整行落盘 + 行键渲染,避免随累计下载线性爆炸。
结论:累计处理数据量 ≠ 主线程常驻内存。常规业务稳定在约 350 MB 左右;极端场景也能把水位压在约 1.2 GB 左右,而不是把每次 Response 永久堆在页面状态里。具体环境仍应以 DevTools Memory / Performance 校准。
证据口径:常规档——瞬时 Main ≈ 521 MB,稳定 Main ≈ 370 MB(表述为约 350 MB 左右);极端档——四次合计 1,050,676 KB,Main ≈ 1,235 MB(表述为约 1,236 MB)。