-
Notifications
You must be signed in to change notification settings - Fork 1.2k
perf(web): make the kimi web host usable on slow links and bound browser load #3706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
REtoolsx
wants to merge
13
commits into
MoonshotAI:main
from
REtoolsx:kimi-web-host-performance-5b01eb
+2,757
−351
Closed
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
b5ae210
perf(web): make the kimi web host usable on slow links and bound brow…
REtoolsx 9818ce4
fix(remote-control): drop browser WebSocket handshake headers on the …
REtoolsx 9ecc53b
fix: keep the unsized sessions list unbounded and gate chunked respon…
REtoolsx ee738db
fix(kap-server): honor Accept-Encoding q-weights and expose experimen…
REtoolsx 066aa69
Merge origin/main into kimi-web-host-performance-5b01eb
REtoolsx 83d82bd
fix(remote-control): honor the chunked-responses flag for manager-cre…
REtoolsx 0d31b4b
fix(web): address review findings on the web host performance changes
REtoolsx 9f18f97
fix(remote-control): let the experimental master switch enable chunke…
REtoolsx be02b15
Merge origin/main into kimi-web-host-performance-5b01eb
REtoolsx e583d36
fix(kap-server): keep the unsized transcript ops catch-up unbounded
REtoolsx 5afd72a
Merge origin/main into kimi-web-host-performance-5b01eb
REtoolsx 2f7a394
fix(kap-server): queue immediate envelopes behind a deferred backlog …
REtoolsx 028c709
fix(remote-control): resume the bridge pump as soon as the tunnel dra…
REtoolsx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Let browsers cache `kimi web --remote-control` UI assets across page loads and add reconnect jitter. Chunked tunnel responses ship as the `remote_control_chunked_responses` experimental feature (`KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL_CHUNKED_RESPONSES=1`). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| The sessions list API now applies its filters while collecting so paged responses are full and `has_more` is accurate (an unsized request still returns the whole list), and the transcript ops catch-up API accepts an optional `limit` and reports `has_more` (an unsized request still returns every batch). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Serve the `kimi web` UI with Brotli/gzip-compressed assets and browser cache revalidation, and fix the content types of wasm and font files. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Batch streamed transcript text and enable WebSocket compression for the `kimi web` UI. Tune with `KIMI_CODE_TRANSCRIPT_OPS_BATCH_MS` and the `KIMI_CODE_WS_*` environment variables. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| #!/usr/bin/env node | ||
| // Emit precompressed `.br` / `.gz` siblings next to the committed web bundle | ||
| // (apps/kimi-code/dist-web) so kap-server can answer `Accept-Encoding` | ||
| // negotiation by streaming a sibling instead of compressing on the fly. | ||
| // | ||
| // Siblings are derived artifacts: they are gitignored, regenerated by the | ||
| // package build, and pruned here when their base file disappears. Only | ||
| // siblings this script could have produced (`<compressible>.br|.gz`) are ever | ||
| // pruned; a `foo.tar.gz` or a bare `foo.br` in the bundle is left alone. The | ||
| // script is mtime-idempotent for hashed bundles — a sibling at least as new as | ||
| // its source is left alone unless `--force` is given. Unhashed files | ||
| // (`index.html`, `boot.js`) are tiny and always regenerated: their name does not | ||
| // change with their content, so mtimes are not a trustworthy signal for them. | ||
| // | ||
| // Usage: node scripts/precompress-web-assets.mjs [--check] [--force] | ||
|
|
||
| import { readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'; | ||
| import { basename, dirname, extname, join, relative, resolve } from 'node:path'; | ||
| import { fileURLToPath, pathToFileURL } from 'node:url'; | ||
| import { parseArgs } from 'node:util'; | ||
| import { brotliCompressSync, constants, gzipSync } from 'node:zlib'; | ||
|
|
||
| const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const DEFAULT_DIST_DIR = resolve(appRoot, 'dist-web'); | ||
|
|
||
| // Text-like bundle outputs that compress well. Everything else (fonts, images, | ||
| // icons, rive animations) is either already compressed or binary-opaque. | ||
| const COMPRESSIBLE_EXTENSIONS = new Set([ | ||
| '.html', | ||
| '.js', | ||
| '.mjs', | ||
| '.css', | ||
| '.svg', | ||
| '.json', | ||
| '.map', | ||
| '.wasm', | ||
| '.txt', | ||
| ]); | ||
| const MIN_SOURCE_BYTES = 1024; | ||
| // A sibling only earns its place when it shaves at least this fraction off. | ||
| const MIN_SAVINGS_RATIO = 0.1; | ||
| const ENTRY_ASSET_PATTERN = /^index-[A-Za-z0-9_-]+\.(?:js|css)$/; | ||
| // Vite output whose name carries a content hash (`index-Dy7xs5tu.js`, | ||
| // `font.Ab12Cd34.woff2`); only these can trust the mtime up-to-date skip. | ||
| const HASHED_FILE_PATTERN = /[-.][A-Za-z0-9_-]{8}\.[^.]+$/; | ||
|
|
||
| const FORMATS = { | ||
| br: { | ||
| extension: '.br', | ||
| compress: (source) => | ||
| brotliCompressSync(source, { | ||
| params: { | ||
| [constants.BROTLI_PARAM_QUALITY]: 11, | ||
| [constants.BROTLI_PARAM_SIZE_HINT]: source.length, | ||
| }, | ||
| }), | ||
| }, | ||
| gz: { | ||
| extension: '.gz', | ||
| compress: (source) => gzipSync(source, { level: 9 }), | ||
| }, | ||
| }; | ||
| const SIBLING_EXTENSIONS = new Set(Object.values(FORMATS).map((format) => format.extension)); | ||
|
|
||
| const isMain = | ||
| process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href; | ||
| if (isMain) { | ||
| try { | ||
| const options = parseCliArgs(process.argv.slice(2)); | ||
| const summary = await precompressWebAssets({ distDir: DEFAULT_DIST_DIR, ...options }); | ||
| console.log( | ||
| options.check | ||
| ? `[precompress-web-assets] OK: entry bundles in ${DEFAULT_DIST_DIR} have .br siblings` | ||
| : formatSummary(summary), | ||
| ); | ||
| } catch (error) { | ||
| process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * @param {{ distDir: string, check?: boolean, force?: boolean }} options | ||
| * @returns {Promise<{ processed: number, written: number, skipped: number, removed: number, bytesBefore: number, bytesAfter: number }>} | ||
| */ | ||
| export async function precompressWebAssets({ distDir, check = false, force = false }) { | ||
| const files = await listFiles(distDir); | ||
| if (check) { | ||
| await assertEntryAssetsPrecompressed(distDir, files); | ||
| return { processed: 0, written: 0, skipped: 0, removed: 0, bytesBefore: 0, bytesAfter: 0 }; | ||
| } | ||
|
|
||
| const formats = Object.values(FORMATS); | ||
| const summary = { processed: 0, written: 0, skipped: 0, removed: 0, bytesBefore: 0, bytesAfter: 0 }; | ||
| const present = new Set(files); | ||
|
|
||
| for (const file of files) { | ||
| if (SIBLING_EXTENSIONS.has(extname(file))) { | ||
| const base = file.slice(0, -extname(file).length); | ||
| if (isCompressible(base) && !present.has(base)) { | ||
| await unlink(file); | ||
| summary.removed++; | ||
| } | ||
| continue; | ||
| } | ||
| if (!isCompressible(file)) { | ||
| continue; | ||
| } | ||
| const sourceStats = await stat(file); | ||
| if (sourceStats.size < MIN_SOURCE_BYTES) { | ||
| continue; | ||
| } | ||
| summary.processed++; | ||
| summary.bytesBefore += sourceStats.size; | ||
| const emitted = await emitSiblings(file, sourceStats, formats, force, summary); | ||
| summary.bytesAfter += emitted; | ||
| } | ||
| return summary; | ||
| } | ||
|
|
||
| async function emitSiblings(file, sourceStats, formats, force, summary) { | ||
| let source; | ||
| let smallest = sourceStats.size; | ||
| const reuseUpToDate = !force && HASHED_FILE_PATTERN.test(basename(file)); | ||
| for (const format of formats) { | ||
| const siblingPath = `${file}${format.extension}`; | ||
| const existing = await stat(siblingPath).catch(() => undefined); | ||
| if (reuseUpToDate && existing !== undefined && existing.mtimeMs >= sourceStats.mtimeMs) { | ||
| summary.skipped++; | ||
| smallest = Math.min(smallest, existing.size); | ||
| continue; | ||
| } | ||
| source ??= await readFile(file); | ||
| const compressed = format.compress(source); | ||
| if (compressed.length > source.length * (1 - MIN_SAVINGS_RATIO)) { | ||
| if (existing !== undefined) { | ||
| await unlink(siblingPath); | ||
| } | ||
| continue; | ||
| } | ||
| await writeSiblingAtomically(siblingPath, compressed); | ||
| summary.written++; | ||
| smallest = Math.min(smallest, compressed.length); | ||
| } | ||
| return smallest; | ||
| } | ||
|
|
||
| // A sibling is written to a temp name and renamed into place so an interrupted | ||
| // build never leaves a truncated `.br`/`.gz` that the server would trust. | ||
| async function writeSiblingAtomically(siblingPath, data) { | ||
| const tempPath = `${siblingPath}.${process.pid}.tmp`; | ||
| await writeFile(tempPath, data); | ||
| await rename(tempPath, siblingPath); | ||
| } | ||
|
|
||
| // Mirrors the runtime rule in kap-server's webAssets route: a sibling only | ||
| // counts when it exists and is at least as new as its source. Entry files the | ||
| // writer would skip (under MIN_SOURCE_BYTES) are not required to have one. | ||
| async function assertEntryAssetsPrecompressed(distDir, files) { | ||
| const entryFiles = files | ||
| .filter((file) => dirname(file) === join(distDir, 'assets')) | ||
| .filter((file) => ENTRY_ASSET_PATTERN.test(relative(join(distDir, 'assets'), file))); | ||
| const missing = []; | ||
| for (const file of entryFiles) { | ||
| const sourceStats = await stat(file); | ||
| if (sourceStats.size < MIN_SOURCE_BYTES) continue; | ||
| const sibling = await stat(`${file}${FORMATS.br.extension}`).catch(() => undefined); | ||
| if (sibling === undefined || sibling.mtimeMs < sourceStats.mtimeMs) { | ||
| missing.push(relative(distDir, file)); | ||
| } | ||
| } | ||
| if (missing.length > 0) { | ||
| throw new Error( | ||
| `Precompressed web assets are missing a .br sibling for: ${missing.join(', ')}. ` + | ||
| 'Run `pnpm --filter @moonshot-ai/kimi-code run precompress:web` and rebuild.', | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| function isCompressible(file) { | ||
| return COMPRESSIBLE_EXTENSIONS.has(extname(file).toLowerCase()); | ||
| } | ||
|
|
||
| async function listFiles(dir) { | ||
| const entries = await readdir(dir, { recursive: true, withFileTypes: true }); | ||
| return entries | ||
| .filter((entry) => entry.isFile()) | ||
| .map((entry) => join(entry.parentPath, entry.name)) | ||
| .sort(); | ||
| } | ||
|
|
||
| function formatSummary(summary) { | ||
| const ratio = | ||
| summary.bytesBefore === 0 ? 1 : summary.bytesAfter / summary.bytesBefore; | ||
| return ( | ||
| `[precompress-web-assets] ${summary.processed} files processed, ` + | ||
| `${summary.written} siblings written, ${summary.skipped} up to date, ${summary.removed} orphans removed; ` + | ||
| `${formatBytes(summary.bytesBefore)} -> ${formatBytes(summary.bytesAfter)} (${(ratio * 100).toFixed(0)}%)` | ||
| ); | ||
| } | ||
|
|
||
| function formatBytes(bytes) { | ||
| return bytes >= 1024 * 1024 | ||
| ? `${(bytes / (1024 * 1024)).toFixed(1)} MB` | ||
| : `${(bytes / 1024).toFixed(0)} KB`; | ||
| } | ||
|
|
||
| function parseCliArgs(args) { | ||
| return parseArgs({ | ||
| args, | ||
| options: { check: { type: 'boolean' }, force: { type: 'boolean' } }, | ||
| strict: true, | ||
| }).values; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.