Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ jobs:
MCP_REF: ${{ inputs.mcp-ref }}
run: npm run bundle:mcp

# The GemDB Stats web build, downloaded from the GemDB Stats release
# pinned in vendor-pins.sh and refused unless its SHA-256 matches. No
# Flutter SDK here: GemDB Stats builds it and publishes it. Platform-
# neutral like the MCP payload, and fetched per leg for the same reason.
- name: Fetch the GemDB Stats web build
run: npm run bundle:stats

# Not a shipped artifact: GemDB installs Grail into whatever database the
# user already has, so nothing here ships an extent. This one exists so
# the tests that need Python but are not testing the file-in do not pay
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ grail/
# but it is still the mcp_server repository's code, not this one's.
mcp/

# The GemDB Stats payload is a build artifact of scripts/bundle-stats.mjs: a
# Flutter web build from the GemDB_Stats repository, made hostable in a webview.
stats/

# The extent the integration suite starts from, built by
# scripts/build-test-extent.sh. A TEST artifact: no extent is shipped, because
# a user's database is theirs and Grail is installed into it.
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
out
grail
stats
*.vsix

# Vendored from Jasper byte-for-byte so upstream fixes are a plain copy.
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ this one does not look.

### Added

- **Statmon files open in GemDB Stats, inside the editor.** Click a `.out.gz` or a `statmon….out`
file, right-click any `.out` and choose **Open in GemDB Stats**, or run
**GemDB: Open Statistics File…**, and the file opens in a tab of GemTalk's statmon viewer: browse
processes, plot statistics, compare them on one chart. Clicking the file name opens another file
in its place, through VS Code's own dialog, so under code-server it browses the server where the
files are. Large files load in the background with a progress bar, and the tabs come back when
the window reloads. It needs no database, so it works before GemDB is set up.
- **The database reserves the free license's full 10 GB, with room to collect its garbage.** GemDB
Code caps the database at the license's 10 GB (until now it stopped at 8 GB), reserves all of it
on disk when the database first starts, and keeps 500 MB of it free, so the database protects
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ npm run test:integration # a real database in a temp root path; seconds
npm run bundle # esbuild -> out/extension.js
npm run bundle:grail # assemble the Grail payload (needs a C toolchain)
npm run bundle:mcp # assemble the MCP server payload (needs nothing)
npm run bundle:stats # fetch the pinned GemDB Stats web build (or STATS_WEB=<build/web>)
npm run test:extent # build the extent the integration suite starts from
npm run package # .vsix
npm run dev:fresh # this checkout in a throwaway profile + empty root path (also :cached-engine, :keep-root)
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ changing it.
| [`grail.md`](grail.md) | how the Grail payload is built and staged, the Python↔GCI bridge (exit codes, `input()`, `print()`, encoding), and the gotchas found building on it | touching the bridge or building an application on Grail |
| [`mcp-server.md`](mcp-server.md) | the bundled MCP server: payload, session cost and leak, read-only mode, stop order, client registration | changing how the MCP server is built, started, stopped or exposed |
| [`repository-space.md`](repository-space.md) | the 10 GB cap, reserved on disk, and the free-space threshold; what the stone does below it (stops reclaiming garbage, refuses and ends `gemdb` sessions); the `gemdb` account and what needs DataCurator; why idle sessions are aborted; what a garbage collection has to wait for | changing the stone's space settings, garbage collection, database accounts, or anything that holds a session open |
| [`statistics.md`](statistics.md) | GemDB Stats in a webview: why a webview, the message protocol with GemDB Stats, where the web build comes from and what `bundle-stats.mjs` checks and changes, and what is still unmeasured | changing how statmon files are opened or how the GemDB Stats build is bundled |
| [`reaching-windows.md`](reaching-windows.md) | the options for a Windows story (WSL, remote server, Docker) and what still needs a Windows machine | starting any Windows work |
| [`demo/`](demo/) | one self-contained directory per demo, with measured findings | building something a demo already explored, or adding a demo |
116 changes: 116 additions & 0 deletions docs/statistics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# GemDB Stats

GemDB Code shows [GemDB Stats](https://github.com/GemTalk/GemDB_Stats),
GemTalk's statmon viewer, in an editor tab. A Flutter web build of it ships in
the `.vsix` as `stats/`. `src/statistics.ts` hosts it, and
`scripts/bundle-stats.mjs` prepares it. GemDB Stats' side of the arrangement is
its `docs/embedding.md`.

## Why a webview

The editor is not always on the machine that holds the statmon files. Under
code-server the only screen is a browser tab, so a desktop app cannot appear
there. A webview is an iframe in that tab whose files the remote machine
serves, so one build works in desktop VS Code and in hosted VS Code. It needs
no port, no server process and no login of its own.

Viewing statistics needs no database. The commands are registered before the
platform gate, so they work on machines that cannot host a database.

## Opening a file

GemDB Stats is a read-only custom editor, `gemdb.statistics`, and the default
editor for `*.out.gz` and `statmon*.out`, so a click on either opens it. A
`.out.gz` is unreadable in the text editor anyway, and statmon names its files
`statmon<pid>.out` or `statmonitor_<stone>_<date>.out`. Any other `.out` (such
as `a.out`, a compiled program) still opens as text. It was `option` for every
`.out` at first, and a click on a `.out.gz` then showed the text editor's
"binary or unsupported encoding" page.

- **GemDB: Open Statistics File…** shows VS Code's open dialog, then opens a
tab on the file.
- **Open in GemDB Stats** opens a `.out` or `.out.gz` from the explorer's
right-click menu: every selected file, or, run without one (from a
keybinding), a file picked in the dialog.
- **Open With… → GemDB Stats** is offered for the same files.
- Each file gets one tab (`supportsMultipleEditorsPerDocument: false`), so a
file already open is brought forward, not parsed a second time. Tabs open
out of preview mode, or opening several from the explorer would leave only
the last.
- `retainContextWhenHidden` keeps a hidden tab's page alive. Bringing a
discarded one back would mean starting Flutter and parsing the file again.
- VS Code brings the tabs back after a window reload by resolving the editor
again: the page is rebuilt, says `ready`, and is sent its file.

## The protocol

Three messages, as GemDB Stats' `docs/embedding.md` specifies:

| Message | Direction | GemDB Code's part |
| --- | --- | --- |
| `{type: 'ready'}` | app → GemDB Code | Answer with `open` for the tab's file, every time: a reloaded page says `ready` again. |
| `{type: 'open', url, name}` | GemDB Code → app | `url` is `asWebviewUri(file)`. Sent only in answer to `ready`. |
| `{type: 'pickFile'}` | app → GemDB Code | The user clicked the file name. Show VS Code's open dialog, then show the picked file in a new tab in the same place, and close this one. |

`pickFile` uses VS Code's dialog rather than the browser's because under
code-server the browser's dialog browses the user's own machine, not the
server where the statmon files are.

Each tab shows one file for as long as it is open, and its page can fetch
only under the panel's `localResourceRoots`: the build and that file's
folder. The folder is as narrow as it can be: VS Code refuses a request for a
resource that is itself a root (read in VS Code 1.140's loader). A picked file
gets a new tab rather than widening this one's roots, because widening them
reloads the page, leaves it able to read every folder it was ever shown, and
an `open` posted across the reload could make the app parse a large file
twice. A picked file already open in another tab brings that tab forward.

## The build

A GemDB Stats release publishes `GemDB-Stats-<version>-web.tar.gz` beside its
desktop builds, from v1.1.0 on. `vendor-pins.sh` pins its URL and SHA-256, and
`npm run bundle:stats` downloads it, checks it and assembles `stats/`. GemDB
Code's CI does not carry the Flutter SDK, which is why it uses a release asset
rather than building a pinned commit, as Grail and the MCP server do. For a
local build, set `STATS_WEB` to `GemDB_Stats/app/build/web`.

`bundle-stats.mjs` refuses a build a webview cannot run:

- one that loads CanvasKit from Google's CDN, which the CSP refuses: build with
`--no-web-resources-cdn`;
- one that offers a Wasm build, whose renderers the payload leaves out: build
without `--wasm`;
- one whose bootstrap registers a service worker, which a webview cannot host.
GemDB Stats' own bootstrap template registers none, from PR #10 on.

Its changes to the page are few: a CSP first in `<head>`, `<base href>`
pointing at the webview, and a nonce on every script. They are made at bundle
time, into a `host.html` template, so a page GemDB Code cannot host fails in
front of whoever is packaging it. `fillHostPage` in `statistics.ts` fills the
three `{{GEMDB_*}}` placeholders for each panel.

It also leaves out what a webview never loads: the Wasm renderers, debug
symbols, the service worker, and the PWA manifest and icons. That cuts the
47 MB build to about 23 MB. `check-vsix.sh` asserts the payload is present and
those files are not.

## Measured

- **2026-10-04, headless Chrome 154.** The generated page, with the full CSP.
Flutter drew its first frame, there were no CSP violations and no service
worker, and the page could fetch a statmon file.
- **2026-10-05, VS Code.** The app showed its loading screen, then grey.
Chrome reproduced it once the page and the build were on different origins,
as they are in a webview: Flutter's first `history.replaceState` resolved its
URL against `<base href>` and threw a SecurityError. The first test used one
origin and could not see it. GemDB Stats fixed it at the source in PR #10:
the app sets no URL strategy, so Flutter never touches the history.

## Open

- **Not yet seen in code-server.**
- **Progress on a large file in VS Code.** A 258 MB `.out.gz` showed progress
and loaded in under 40 s in Chrome. VS Code 1.140's webview resource loader
streams files in chunks rather than reading them whole first (read in its
source, 2026-10-06), so the progress bar should move there too. Not yet
watched in VS Code.
13 changes: 11 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@ export default tseslint.config(
// it. The rest are build artifacts that ESLint would otherwise walk on
// its own, since (unlike Prettier 3) it does not read .gitignore: out/**
// is this project's own bundle, grail/** and mcp/** are third-party
// payloads staged by bundle-grail.sh and bundle-mcp.sh, dist/** is
// payloads staged by bundle-grail.sh and bundle-mcp.sh, stats/** is the
// GemDB Stats web build staged by bundle-stats.mjs, dist/** is
// packages fetched from CI by fetch-vsix.sh, and .test-extent/** is the
// database extent build-test-extent.sh builds for the integration suite.
ignores: ['out/**', 'grail/**', 'mcp/**', 'dist/**', '.test-extent/**', 'src/gci/**'],
ignores: [
'out/**',
'grail/**',
'mcp/**',
'stats/**',
'dist/**',
'.test-extent/**',
'src/gci/**',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
Expand Down
39 changes: 39 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,21 @@
"when": "gemdb.running && gemdb.hasSavedStacks"
}
],
"customEditors": [
{
"viewType": "gemdb.statistics",
"displayName": "GemDB Stats",
"selector": [
{
"filenamePattern": "*.out.gz"
},
{
"filenamePattern": "statmon*.out"
}
],
"priority": "default"
}
],
"commands": [
{
"command": "gemdb.install",
Expand Down Expand Up @@ -323,6 +338,18 @@
"category": "GemDB",
"icon": "$(output)"
},
{
"command": "gemdb.openStatistics",
"title": "Open Statistics File…",
"category": "GemDB",
"icon": "$(graph-line)"
},
{
"command": "gemdb.openInStats",
"title": "Open in GemDB Stats",
"category": "GemDB",
"icon": "$(graph-line)"
},
{
"command": "gemdb.copyTelemetryId",
"title": "Copy Telemetry ID",
Expand Down Expand Up @@ -571,6 +598,10 @@
{
"command": "gemdb.restoreStack",
"when": "gemdb.running && gemdb.hasSavedStacks"
},
{
"command": "gemdb.openInStats",
"when": "false"
}
],
"debug/variables/context": [
Expand Down Expand Up @@ -623,6 +654,13 @@
"when": "debugType == gemdb && !gemdb.savedStackOpen",
"group": "z_gemdb@1"
}
],
"explorer/context": [
{
"command": "gemdb.openInStats",
"when": "resourceFilename =~ /\\.out(\\.gz)?$/",
"group": "navigation@9"
}
]
},
"walkthroughs": [
Expand Down Expand Up @@ -719,6 +757,7 @@
"test:integration": "vitest run --config vitest.integration.config.mts",
"bundle:grail": "./scripts/bundle-grail.sh",
"bundle:mcp": "./scripts/bundle-mcp.sh",
"bundle:stats": "node scripts/bundle-stats.mjs",
"test:extent": "./scripts/build-test-extent.sh",
"//dev:fresh": "Launch this working copy in a separate editor on a throwaway profile (empty global storage) and an empty gemdb.rootPath: a brand-new user's first run, engine download included. Machine-wide shared memory is not reset; see the script's header.",
"dev:fresh": "bash scripts/dev-fresh.sh",
Expand Down
Loading
Loading