From 9ca0a10a37bc4ed1809230137d2da6db109b6102 Mon Sep 17 00:00:00 2001 From: bbopen Date: Fri, 16 Jan 2026 18:13:09 -0800 Subject: [PATCH 1/3] feat(ci): add codec perf budgets --- .github/workflows/ci.yml | 7 ++- docs/configuration.md | 6 +++ test/codec-performance.test.ts | 79 ++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/codec-performance.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a81e1c..c987bb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,12 @@ jobs: run: python test/python/library_integration.py --suite all codec-suite: - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + if: >- + github.event_name == 'schedule' || + github.event_name == 'workflow_dispatch' || + github.event_name == 'push' || + (github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'area:codec')) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/docs/configuration.md b/docs/configuration.md index fcd9f0a..4be454b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -301,6 +301,12 @@ export TYWRAP_TORCH_ALLOW_COPY="1" # Performance tuning export TYWRAP_CACHE_DIR="./.tywrap/cache" export TYWRAP_MEMORY_LIMIT="1024" +export TYWRAP_PERF_BUDGETS="1" +export TYWRAP_PERF_TIME_BUDGET_MS="2000" +export TYWRAP_PERF_MEMORY_BUDGET_MB="64" +export TYWRAP_CODEC_PERF_ITERATIONS="200" +export TYWRAP_CODEC_PERF_TIME_BUDGET_MS="500" +export TYWRAP_CODEC_PERF_MEMORY_BUDGET_MB="32" # Development export TYWRAP_VERBOSE="true" diff --git a/test/codec-performance.test.ts b/test/codec-performance.test.ts new file mode 100644 index 0000000..23529cc --- /dev/null +++ b/test/codec-performance.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { decodeValue } from '../src/utils/codec.js'; +import { isNodejs } from '../src/utils/runtime.js'; + +const shouldRun = isNodejs() && (process.env.CI || process.env.TYWRAP_PERF_BUDGETS === '1'); +const describeBudget = shouldRun ? describe : describe.skip; + +describeBudget('Codec performance budgets', () => { + it('decodes representative envelopes within time/memory budgets', () => { + const iterations = Number(process.env.TYWRAP_CODEC_PERF_ITERATIONS ?? '200'); + const timeBudgetMs = Number(process.env.TYWRAP_CODEC_PERF_TIME_BUDGET_MS ?? '500'); + const memoryBudgetMb = Number(process.env.TYWRAP_CODEC_PERF_MEMORY_BUDGET_MB ?? '32'); + + const sparseEnvelope = { + __tywrap__: 'scipy.sparse', + codecVersion: 1, + encoding: 'json', + format: 'csr', + shape: [100, 100], + data: Array.from({ length: 200 }, (_, idx) => idx % 7), + indices: Array.from({ length: 200 }, (_, idx) => idx % 100), + indptr: Array.from({ length: 101 }, (_, idx) => Math.min(idx * 2, 200)), + } as const; + + const torchEnvelope = { + __tywrap__: 'torch.tensor', + codecVersion: 1, + encoding: 'ndarray', + value: { + __tywrap__: 'ndarray', + codecVersion: 1, + encoding: 'json', + data: [ + [1, 2], + [3, 4], + ], + shape: [2, 2], + }, + shape: [2, 2], + dtype: 'float32', + device: 'cpu', + } as const; + + const sklearnEnvelope = { + __tywrap__: 'sklearn.estimator', + codecVersion: 1, + encoding: 'json', + className: 'LinearRegression', + module: 'sklearn.linear_model._base', + version: '1.4.2', + params: { + fit_intercept: true, + copy_X: true, + }, + } as const; + + if (global.gc) { + global.gc(); + } + const startMem = process.memoryUsage().heapUsed; + const start = performance.now(); + + for (let i = 0; i < iterations; i += 1) { + decodeValue(sparseEnvelope); + decodeValue(torchEnvelope); + decodeValue(sklearnEnvelope); + } + + const duration = performance.now() - start; + if (global.gc) { + global.gc(); + } + const endMem = process.memoryUsage().heapUsed; + const deltaMem = endMem - startMem; + + expect(duration).toBeLessThan(timeBudgetMs); + expect(deltaMem).toBeLessThan(memoryBudgetMb * 1024 * 1024); + }); +}); From ea4215cc44c977521d69986ed0b4801fc28c0dd6 Mon Sep 17 00:00:00 2001 From: bbopen Date: Fri, 16 Jan 2026 21:06:49 -0800 Subject: [PATCH 2/3] refactor(test): simplify perf budget helpers --- test/codec-performance.test.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/test/codec-performance.test.ts b/test/codec-performance.test.ts index 23529cc..4b3a456 100644 --- a/test/codec-performance.test.ts +++ b/test/codec-performance.test.ts @@ -5,11 +5,21 @@ import { isNodejs } from '../src/utils/runtime.js'; const shouldRun = isNodejs() && (process.env.CI || process.env.TYWRAP_PERF_BUDGETS === '1'); const describeBudget = shouldRun ? describe : describe.skip; +const readEnvNumber = (name: string, fallback: string): number => + Number(process.env[name] ?? fallback); + +const runGc = (): void => { + if (global.gc) { + global.gc(); + } +}; + describeBudget('Codec performance budgets', () => { it('decodes representative envelopes within time/memory budgets', () => { - const iterations = Number(process.env.TYWRAP_CODEC_PERF_ITERATIONS ?? '200'); - const timeBudgetMs = Number(process.env.TYWRAP_CODEC_PERF_TIME_BUDGET_MS ?? '500'); - const memoryBudgetMb = Number(process.env.TYWRAP_CODEC_PERF_MEMORY_BUDGET_MB ?? '32'); + const iterations = readEnvNumber('TYWRAP_CODEC_PERF_ITERATIONS', '200'); + const timeBudgetMs = readEnvNumber('TYWRAP_CODEC_PERF_TIME_BUDGET_MS', '500'); + const memoryBudgetMb = readEnvNumber('TYWRAP_CODEC_PERF_MEMORY_BUDGET_MB', '32'); + const memoryBudgetBytes = memoryBudgetMb * 1024 * 1024; const sparseEnvelope = { __tywrap__: 'scipy.sparse', @@ -54,9 +64,7 @@ describeBudget('Codec performance budgets', () => { }, } as const; - if (global.gc) { - global.gc(); - } + runGc(); const startMem = process.memoryUsage().heapUsed; const start = performance.now(); @@ -67,13 +75,11 @@ describeBudget('Codec performance budgets', () => { } const duration = performance.now() - start; - if (global.gc) { - global.gc(); - } + runGc(); const endMem = process.memoryUsage().heapUsed; const deltaMem = endMem - startMem; expect(duration).toBeLessThan(timeBudgetMs); - expect(deltaMem).toBeLessThan(memoryBudgetMb * 1024 * 1024); + expect(deltaMem).toBeLessThan(memoryBudgetBytes); }); }); From 2fb604a53cc715ec9b5fd3700e5b210767bf5cb7 Mon Sep 17 00:00:00 2001 From: bbopen Date: Sat, 17 Jan 2026 06:32:38 -0800 Subject: [PATCH 3/3] fix(test): align perf budget gating --- test/codec-performance.test.ts | 3 ++- test/performance-budgets.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/codec-performance.test.ts b/test/codec-performance.test.ts index 4b3a456..3c2af62 100644 --- a/test/codec-performance.test.ts +++ b/test/codec-performance.test.ts @@ -1,8 +1,9 @@ +import { performance } from 'node:perf_hooks'; import { describe, it, expect } from 'vitest'; import { decodeValue } from '../src/utils/codec.js'; import { isNodejs } from '../src/utils/runtime.js'; -const shouldRun = isNodejs() && (process.env.CI || process.env.TYWRAP_PERF_BUDGETS === '1'); +const shouldRun = isNodejs() && process.env.TYWRAP_PERF_BUDGETS === '1'; const describeBudget = shouldRun ? describe : describe.skip; const readEnvNumber = (name: string, fallback: string): number => diff --git a/test/performance-budgets.test.ts b/test/performance-budgets.test.ts index 6656f71..451c009 100644 --- a/test/performance-budgets.test.ts +++ b/test/performance-budgets.test.ts @@ -1,3 +1,4 @@ +import { performance } from 'node:perf_hooks'; import { describe, it, expect } from 'vitest'; import { CodeGenerator } from '../src/core/generator.js'; import type { @@ -9,7 +10,7 @@ import type { } from '../src/types/index.js'; import { isNodejs } from '../src/utils/runtime.js'; -const shouldRun = isNodejs() && (process.env.CI || process.env.TYWRAP_PERF_BUDGETS === '1'); +const shouldRun = isNodejs() && process.env.TYWRAP_PERF_BUDGETS === '1'; const describeBudget = shouldRun ? describe : describe.skip; function primitiveType(name: 'int' | 'str' | 'bool' | 'float' | 'bytes' | 'None'): PythonType {