From 971cbfc7460517ec9ca98f3f6771383fab9a49a0 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Thu, 19 Sep 2024 17:44:10 +0300 Subject: [PATCH] perf: optimize `ProsessOutput` mem consumption (#903) * perf: optimize `ProsessOutput` mem consumption * style: simplify process end handler * chore(types): define `ProcessOutput` overload signature --- .size-limit.json | 8 +-- package-lock.json | 8 +-- package.json | 2 +- src/core.ts | 119 +++++++++++++++++++++++++++--------------- src/util.ts | 10 ++++ test-d/core.test-d.ts | 16 +++++- 6 files changed, 110 insertions(+), 53 deletions(-) diff --git a/.size-limit.json b/.size-limit.json index 4a7b4c4d31..434e4981e0 100644 --- a/.size-limit.json +++ b/.size-limit.json @@ -2,21 +2,21 @@ { "name": "zx/core", "path": ["build/core.cjs", "build/util.cjs", "build/vendor-core.cjs"], - "limit": "70 kB", + "limit": "71 kB", "brotli": false, "gzip": false }, { "name": "zx/index", "path": "build/*.{js,cjs}", - "limit": "796 kB", + "limit": "797 kB", "brotli": false, "gzip": false }, { "name": "dts libdefs", "path": "build/*.d.ts", - "limit": "35 kB", + "limit": "36 kB", "brotli": false, "gzip": false }, @@ -30,7 +30,7 @@ { "name": "all", "path": "build/*", - "limit": "830 kB", + "limit": "832 kB", "brotli": false, "gzip": false } diff --git a/package-lock.json b/package-lock.json index 3e807b26e1..6fb6832b7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "typescript": "^5.6.2", "which": "^4.0.0", "yaml": "^2.5.1", - "zurk": "^0.3.2" + "zurk": "^0.3.4" }, "engines": { "node": ">= 12.17.0" @@ -5557,9 +5557,9 @@ } }, "node_modules/zurk": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/zurk/-/zurk-0.3.2.tgz", - "integrity": "sha512-bMihBxEc0ECXBfZbC3se+4E8jc07QvHh8stapRvPR1hwp8Jq27B7QS/sGqCwuwFWWe7EkK3IwXhhVYEgm0ghuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/zurk/-/zurk-0.3.4.tgz", + "integrity": "sha512-Mu0uXIAgYezo9zprkW/jzjgBo6jkCCGNPpdpQr4W5aRUTDRHCcy9KvOY+cKg6XYfPp0ArqsQsFbeZSf2f8Ka6Q==", "dev": true, "license": "MIT" } diff --git a/package.json b/package.json index 984257ce5b..d721e5ef3f 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "typescript": "^5.6.2", "which": "^4.0.0", "yaml": "^2.5.1", - "zurk": "^0.3.2" + "zurk": "^0.3.4" }, "publishConfig": { "registry": "https://wombat-dressing-room.appspot.com" diff --git a/src/core.ts b/src/core.ts index 3ad6aa4aa2..920dfef3bf 100644 --- a/src/core.ts +++ b/src/core.ts @@ -44,6 +44,7 @@ import { isString, isStringLiteral, noop, + once, parseDuration, preferLocalBin, quote, @@ -320,52 +321,50 @@ export class ProcessPromise extends Promise { // Stderr should be printed regardless of piping. $.log({ kind: 'stderr', data, verbose: !self.isQuiet() }) }, - end: ( - { error, stdout, stderr, stdall, status, signal, duration }, - c - ) => { + // prettier-ignore + end: (data, c) => { self._resolved = true - // Ensures EOL - if (stderr && !stderr.endsWith('\n')) c.on.stderr?.(eol, c) - if (stdout && !stdout.endsWith('\n')) c.on.stdout?.(eol, c) + const { error, status, signal, duration, ctx } = data + const { stdout, stderr, stdall } = ctx.store + + // Lazy getters + const _stdout = once(() => stdout.join('')) + const _stderr = once(() => stderr.join('')) + const _stdall = once(() => stdall.join('')) + const _duration = () => duration + let _code = () => status + let _signal = () => signal + let _message = once(() => ProcessOutput.getExitMessage( + status, + signal, + _stderr(), + self._from + )) + // Ensures EOL + if (stdout.length && !stdout[stdout.length - 1]?.toString().endsWith('\n')) c.on.stdout?.(eol, c) + if (stderr.length && !stderr[stderr.length - 1]?.toString().endsWith('\n')) c.on.stderr?.(eol, c) if (error) { - const message = ProcessOutput.getErrorMessage(error, self._from) - // Should we enable this? - // (nothrow ? self._resolve : self._reject)( - const output = new ProcessOutput( - null, - null, - stdout, - stderr, - stdall, - message, - duration - ) - self._output = output + _code = () => null + _signal = () => null + _message = () => ProcessOutput.getErrorMessage(error, self._from) + } + + const output = new ProcessOutput({ + code: _code, + signal: _signal, + stdout: _stdout, + stderr: _stderr, + stdall: _stdall, + message: _message, + duration: _duration + }) + self._output = output + + if (error || status !== 0 && !self.isNothrow()) { self._reject(output) } else { - const message = ProcessOutput.getExitMessage( - status, - signal, - stderr, - self._from - ) - const output = new ProcessOutput( - status, - signal, - stdout, - stderr, - stdall, - message, - duration - ) - self._output = output - if (status === 0 || self.isNothrow()) { - self._resolve(output) - } else { - self._reject(output) - } + self._resolve(output) } }, }, @@ -572,14 +571,27 @@ export class ProcessPromise extends Promise { } } +type GettersRecord> = { [K in keyof T]: () => T[K] } + +type ProcessOutputLazyDto = GettersRecord<{ + code: number | null + signal: NodeJS.Signals | null + stdout: string + stderr: string + stdall: string + message: string + duration: number +}> + export class ProcessOutput extends Error { - private readonly _code: number | null + private readonly _code: number | null = null private readonly _signal: NodeJS.Signals | null private readonly _stdout: string private readonly _stderr: string private readonly _combined: string private readonly _duration: number + constructor(dto: ProcessOutputLazyDto) constructor( code: number | null, signal: NodeJS.Signals | null, @@ -587,15 +599,36 @@ export class ProcessOutput extends Error { stderr: string, combined: string, message: string, + duration?: number + ) + constructor( + code: number | null | ProcessOutputLazyDto, + signal: NodeJS.Signals | null = null, + stdout: string = '', + stderr: string = '', + combined: string = '', + message: string = '', duration: number = 0 ) { super(message) - this._code = code this._signal = signal this._stdout = stdout this._stderr = stderr this._combined = combined this._duration = duration + if (code !== null && typeof code === 'object') { + Object.defineProperties(this, { + _code: { get: code.code }, + _signal: { get: code.signal }, + _duration: { get: code.duration }, + _stdout: { get: code.stdout }, + _stderr: { get: code.stderr }, + _combined: { get: code.stdall }, + message: { get: code.message }, + }) + } else { + this._code = code + } } toString() { diff --git a/src/util.ts b/src/util.ts index e37be7eaab..277dd8764b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -449,3 +449,13 @@ export function getCallerLocationFromString(stackString = 'unknown') { ?.trim() || stackString ) } + +export const once = any>(fn: T) => { + let called = false + let result: ReturnType + return (...args: Parameters): ReturnType => { + if (called) return result + called = true + return (result = fn(...args)) + } +} diff --git a/test-d/core.test-d.ts b/test-d/core.test-d.ts index 200f35fa2f..6cd9b32242 100644 --- a/test-d/core.test-d.ts +++ b/test-d/core.test-d.ts @@ -14,7 +14,7 @@ import assert from 'node:assert' import { Readable, Writable } from 'node:stream' -import { expectType } from 'tsd' +import { expectError, expectType } from 'tsd' import { $, ProcessPromise, ProcessOutput, within } from 'zx' let p = $`cmd` @@ -40,5 +40,19 @@ expectType(o.stdout) expectType(o.stderr) expectType(o.exitCode) expectType(o.signal) +// prettier-ignore +expectType(new ProcessOutput({ + code() { return null }, + signal() { return null }, + stdall() { return '' }, + stderr() { return '' }, + stdout() { return '' }, + duration() { return 0 }, + message() { return '' }, +})) + +expectType(new ProcessOutput(null, null, '', '', '', '', 1)) +expectType(new ProcessOutput(null, null, '', '', '', '')) +expectError(new ProcessOutput(null, null)) expectType<'banana'>(within(() => 'apple' as 'banana'))