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..3c2af62 --- /dev/null +++ b/test/codec-performance.test.ts @@ -0,0 +1,86 @@ +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.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 = 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', + 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; + + runGc(); + 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; + runGc(); + const endMem = process.memoryUsage().heapUsed; + const deltaMem = endMem - startMem; + + expect(duration).toBeLessThan(timeBudgetMs); + expect(deltaMem).toBeLessThan(memoryBudgetBytes); + }); +}); 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 {