Skip to content

docs(usage): design revision-consistent bounded screen reads - #5023

Draft
Sun-GLiang wants to merge 4 commits into
apache:mainfrom
Sun-GLiang:codex/4058-bounded-usage-design
Draft

docs(usage): design revision-consistent bounded screen reads#5023
Sun-GLiang wants to merge 4 commits into
apache:mainfrom
Sun-GLiang:codex/4058-bounded-usage-design

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Settings Usage should return its statistics, pricing/coverage, and first bounded activity page from one Storage read transaction and one revision through the existing usage:summary IPC. Further pages are on-demand Storage cursor reads checked against that revision. A mismatch refreshes the whole screen; Host retains no per-reader dataset and Desktop never drains pages or aggregates activity rows.

This is a design-only draft for #4058, following the maintainer's P1 review and Storage/protocol rules. It does not implement or claim measured production improvements.

Design: Revision-consistent, bounded Usage screen reads.

Review focus

The document now provides author proposals, code-based trade-offs, a real writer-path matrix, candidate metadata lifecycles, and a concrete bounded-admission option. These are inputs for likun's Storage decisions, not decisions made on his behalf. Product behavior needs separate confirmation. Please accept, replace, or request evidence for each item:

  • Query and repair boundary — likun: existing Usage facade with one internal synchronous query module; one explicit Host-requested repair through the existing writer, followed by the read transaction. A writable Storage wrapper is an alternative. Repair must bound selection and event bytes as well as processed run/event counts.
  • Revision and writer coverage — likun: proposed root-wide counter updated in each originating write transaction, combined with existing pricing revision and query/Host fencing; triggers remain an alternative. Review source commits, canonical/legacy/tool changes, checkpoints, cascades, migration and restore, including repeated counter values and live-write refresh frequency.
  • Schema, accounting and cursor — likun: reuse canonical SQL; review legacy/tool scalar columns, resumable backfill and source-qualified cursor indexes. Preserve historical Usage after Session deletion, additive source accounting, connection-based grouping and unpriced versus zero. Validate the exact seek predicates against SQLite, not merely the index name.
  • Work budgets and limit outcome — likun + product: evaluate indexed bounded admission before exact work. The document proposes starting row ceilings for repair, completeness, aggregates and sparse search; byte/frame caps and acceptance latency still need selection and full-path evidence. Overflow returns an explicit failure, never partial totals. Root-scoped completeness overflow can reject Today/7d as well as All; a narrower range is not always a remedy. If that availability trade-off is unacceptable, choose another bounded architecture before implementation. Query interruption alone does not guarantee exact All availability.
  • Existing filters — product, with Storage feasibility by likun: recommend preserving model/provider/tool substring search and status filtering across the selected range by moving filtering into Storage. Search is an existing feature, not an optional new extension. Removing it or restricting it to one page requires an explicit product decision; preserve Unicode normalization and bound sparse scans.
  • Refresh and other lists — product, with Storage support by likun: propose at most one automatic first-screen reload per user action, then manual Refresh. Choose visible pagination or explicit whole-screen failure for oversized breakdown/pricing collections; no silent truncation or background drain.

Agreement with the core direction permits design progress. Implementation approval still requires the selected mechanisms, budgets, product outcomes, and remaining feasibility evidence to be recorded. Material product decisions follow the project's public discussion process.

Implementation plan

  • Draft the cross-package contract, compare Storage choices, and map real writer/lifecycle paths.
  • Run the narrow SQLite admission/cursor experiment below; keep its limits distinct from implementation validation.
  • Resolve the review decisions above, including byte/frame ceilings and whole-path acceptance criteria.
  • Implement the selected Storage transaction, revision, schema/index and bounded-read behavior; add real-SQLite accounting, race, cursor and migration tests.
  • Add Host protocol and Desktop IPC/preload support; raise the compatibility epoch above main when the wire change lands.
  • Implement on-demand pagination, preserved filters and atomic revision-change refresh with query/Host fencing and the agreed limit states.
  • Record first-screen, aggregation, repair/completeness, deep-page, sparse-filter and active-write evidence; run affected tests, typecheck, lint/format and the epoch guard before ready-for-review.

Verification

  • Static source baseline: 93a8dd785; design review covers the document linked above.
  • Documentation whitespace, staged Biome wrapper and protocol-epoch checks: passed. The document ASF header was checked unchanged; the staged ASF tool covers zero files here. Biome ignores Markdown; no runtime protocol changed and epoch 133 is unchanged on this branch.
  • Node 24.19.0 / SQLite 3.53.3 in-memory experiment: imported the baseline schema migrations, added one candidate (ts DESC, storage_key DESC) index, and generated 10k/50k/250k legacy-source rows. Every display ID is duplicated; ten rows share each timestamp.
  • The admission probe deliberately uses 50k as its experiment ceiling to exercise both acceptance and refusal. That is not the document's proposed 250k aggregate ceiling and does not select a production limit.
Historical rows Admission rows examined (cap + 1) Deep-page predicate visits with redundant range upper bound Visits with validated tuple upper bound
10,000 10,000 9,101 102
50,000 50,000 45,101 102
250,000 50,001 225,101 102

The deep cursor skips about 90% of the rows and returns 101 keys including lookahead. Predicate visits are counted by an instrumented SQLite function, not VM-step or disk-I/O counts; the script verifies that instrumented and uninstrumented EXPLAIN plans match. The redundant form chose (ts>? AND ts<?); removing the redundant upper bound after cursor validation chose (ts>? AND (ts,storage_key)<(?,?)). Neither plan used a temporary sort for this fixture.

This only supports the single-source scalar admission/seek shape. It does not prove multi-source accounting, aggregate plans, repair/completeness bounds, byte budgets, sparse search, migrations, concurrency, physical I/O bounds or end-to-end performance. Runtime tests, build, typecheck, UI exercises and production-path benchmarks have not been run for this documentation-only change.

Reproduce the SQL feasibility probe

At this PR checkout, save the following as an untracked usage-design-probe.mjs in the repository root and run node usage-design-probe.mjs. It creates only in-memory databases; remove the scratch file afterwards. Requires Node with TypeScript stripping and node:sqlite; record the reported Node/SQLite versions when comparing plans.

import { DatabaseSync } from 'node:sqlite';
import { performance } from 'node:perf_hooks';
import assert from 'node:assert/strict';
import { migrateSqliteCoreExecutionDatabase } from './packages/storage/src/sqlite-core-execution-schema.ts';
import { migrateSqliteUsageDatabase } from './packages/storage/src/sqlite-usage-schema.ts';

const result = { node: process.version, fixtures: [] };
for (const n of [10000, 50000, 250000]) {
  const db = new DatabaseSync(':memory:');
  migrateSqliteCoreExecutionDatabase(db);
  migrateSqliteUsageDatabase(db);
  result.sqlite = db.prepare('SELECT sqlite_version() AS version').get().version;
  // Candidate index only: no production schema is modified.
  db.exec('CREATE INDEX candidate_usage_seek ON usage_llm_calls(ts DESC, storage_key DESC)');
  const insert = db.prepare('INSERT INTO usage_llm_calls(storage_key,id,ts,record_json) VALUES (?,?,?,?)');
  db.exec('BEGIN');
  for (let i = 0; i < n; i++) insert.run(String(i).padStart(12,'0'), 'duplicate-display-id', Math.floor(i / 10), '{}');
  db.exec('COMMIT');
  db.exec('ANALYZE');
  const cap = 50000;
  const admissionSql = `SELECT COUNT(*) AS n FROM (
    SELECT ts FROM usage_llm_calls INDEXED BY candidate_usage_seek
    WHERE ts >= ? AND ts <= ?
    ORDER BY ts DESC, storage_key DESC LIMIT ?
  )`;
  const deepSql = `SELECT ts, storage_key FROM usage_llm_calls INDEXED BY candidate_usage_seek
    WHERE ts >= ? AND (ts, storage_key) < (?, ?)
    ORDER BY ts DESC, storage_key DESC LIMIT ?`;
  const deepKey = Math.floor(n / 10);
  const admissionArgs = [0, n, cap + 1];
  const deepArgs = [0, Math.floor(deepKey / 10), String(deepKey).padStart(12,'0'), 101];
  const measure = (sql,args) => {
    const statement = db.prepare(sql);
    statement.all(...args);
    const timings = [];
    let rows;
    for(let i=0;i<5;i++){ const t=performance.now(); rows=statement.all(...args); timings.push(performance.now()-t); }
    return { rows, medianMs: +timings.sort((a,b)=>a-b)[2].toFixed(3) };
  };
  const admission = measure(admissionSql, admissionArgs);
  assert.equal(admission.rows[0].n, Math.min(n, cap + 1));
  const deep = measure(deepSql, deepArgs);
  assert.equal(deep.rows.length,101);
  assert.equal(deep.rows[0].storage_key,String(deepKey-1).padStart(12,'0'));
  let visited = 0;
  db.function('visit', () => {visited++; return 1;});
  db.prepare(admissionSql.replace('WHERE ts >= ?', 'WHERE visit() AND ts >= ?')).get(...admissionArgs);
  const admissionVisits = visited;
  visited = 0;
  db.prepare(deepSql.replace('WHERE ts >= ?', 'WHERE visit() AND ts >= ?')).all(...deepArgs);
  assert.ok(visited <= 102);
  assert.equal(admissionVisits,Math.min(n,cap+1));
  const deepVisits = visited;
  visited = 0;
  const redundantSql = deepSql.replace('WHERE ts >= ?', 'WHERE ts >= ? AND ts <= ?');
  const redundantArgs = [0,n,...deepArgs.slice(1)];
  db.prepare(redundantSql.replace('WHERE ts >= ?', 'WHERE visit() AND ts >= ?')).all(...redundantArgs);
  const redundantVisits = visited;
  result.fixtures.push({n, admitted:admission.rows[0].n<=cap, admissionRows:admission.rows[0].n,
    admissionVisits, admissionMs:admission.medianMs, deepRows:deep.rows.length, deepVisits, redundantVisits, deepMs:deep.medianMs,
    admissionPlan:db.prepare('EXPLAIN QUERY PLAN '+admissionSql).all(...admissionArgs).map(r=>r.detail),
    redundantPlan:db.prepare('EXPLAIN QUERY PLAN '+redundantSql).all(...redundantArgs).map(r=>r.detail),
    deepPlan:db.prepare('EXPLAIN QUERY PLAN '+deepSql).all(...deepArgs).map(r=>r.detail)});
  for (const [sql,args] of [[admissionSql,admissionArgs],[deepSql,deepArgs],[redundantSql,redundantArgs]]) {
    const plan = q => db.prepare('EXPLAIN QUERY PLAN '+q).all(...args).map(r=>r.detail);
    assert.deepEqual(plan(sql),plan(sql.replace('WHERE ts >= ?', 'WHERE visit() AND ts >= ?')));
  }
  db.close();
}
console.log(JSON.stringify(result,null,2));

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex inspected the repository, drafted and reviewed the design and decision tables, and ran the synthetic SQL probe. Sun-GLiang is the contributor of record. Documentation commits carry Generated-by: Codex.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

These implementation checks remain pending; this draft changes documentation only.

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

The current diff has no runtime behavior change. Update this selection when implementation lands.

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 8, 2026
Align the design draft and PR scope around the core P1 contract, leaving concrete Storage boundaries and mechanisms for likun before implementation.

Generated-by: Codex
@github-actions github-actions Bot added effort/L Under 1000 readable lines and removed effort/M Under 500 readable lines labels Sep 9, 2026
@likun666661

Copy link
Copy Markdown
Member

从问题定义和奥卡姆剃刀角度看,我认可核心方向:单个 Storage 事务返回首屏、SQL 聚合、明细按需游标分页、后续页校验 revision,且不保留 Host 侧的整批数据快照。这比用容量、租约、释放协议维护全量副本更直接。

建议把问题定义收敛为:

Usage 应展示口径一致的统计和明细;显示一页不应要求把全量历史物化到应用内存,单次请求的工作量应有明确边界。

这里需要区分三个独立目标:数据一致性、资源有界、产品可用性。前两个成立,不自动意味着第三个成立。文档已经列出相关取舍,但建议在继续细化实现前,先明确两个产品决策:

  1. 数据量超过预算时,是否允许 Usage 不可用? 当前 bounded-admission 候选通过拒绝查询来限制工作量,不是保证任意历史规模下都能返回准确统计。尤其 completeness 是 root/session scoped,超过 source/checkpoint 上限后,Today 和 7d 也可能失败,缩小时间范围并不一定有用。请明确支持规模及超限用户行为。如果长期积累后仍必须可查准确统计,就需要评估增量聚合等架构;这时额外复杂度是需求所必需,不能仅靠提高阈值或增加重试解决。

  2. 持续写入时,是否接受翻页被打断并回到首屏? revision 校验能防止混搭,但不保留旧快照意味着版本变化后不能继续浏览原结果。root-wide counter 还可能因范围外写入而失效。请用持续调用场景验证 continuation 成功率和刷新频率,并明确可接受的交互;限制自动重试次数只能避免无限重试,不能保证可浏览性。

最小实现方向仍建议保留上述核心,以及现有计费、完整性、搜索语义。不要为简化而把未计价当成零费用,或把范围搜索缩成当前页搜索。细粒度 revision、额外列表分页、通用查询抽象则应按实际约束和证据决定,不必预先全部引入;暂不分页的集合仍需明确上限及超限行为。

另外,SQL 聚合减少的是应用侧物化、JSON 解码和传输,不自动消除随历史规模增长的扫描/分组工作;现有单源 SQLite probe 的结论边界应继续保留。

结论:支持核心方向,但先把“数据大了能否拒绝展示”和“写入活跃时能否打断翻页”定清楚,再选择具体 Storage 机制和预算。这是设计意见,不是实现批准。


AI-assisted:本评论由 Codex 根据本任务中的讨论整理并经用户授权发布。基于设计文档 ddae525b#4058 和前序设计评审;未运行实现测试或端到端性能验证。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/L Under 1000 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants