diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/0-valid.ts | 22 | ||||
-rw-r--r-- | test/1-size.ts | 24 | ||||
-rw-r--r-- | test/2-perf.ts | 47 | ||||
-rw-r--r-- | test/3-zip.ts | 1 | ||||
-rw-r--r-- | test/4-streams.ts | 1 | ||||
-rw-r--r-- | test/5-async.ts | 1 | ||||
-rw-r--r-- | test/data/.gitignore | 2 | ||||
-rw-r--r-- | test/results/.gitignore | 2 | ||||
-rw-r--r-- | test/tsconfig.json | 8 | ||||
-rw-r--r-- | test/util.ts | 196 |
10 files changed, 304 insertions, 0 deletions
diff --git a/test/0-valid.ts b/test/0-valid.ts new file mode 100644 index 0000000..99516b4 --- /dev/null +++ b/test/0-valid.ts @@ -0,0 +1,22 @@ +import { testSuites, workers, bClone } from './util'; +import * as assert from 'uvu/assert'; + +// Name is to ensure that this runs first +// Note that workers are not used here to optimize performance but rather +// to prevent infinite loops from hanging the process. +testSuites({ + async compression(file) { + const fileClone = bClone(file); + const cProm = workers.fflate.deflate(fileClone, [fileClone.buffer]); + cProm.timeout(10000); + const buf = await cProm; + assert.ok(file.equals(await workers.zlib.inflate(buf, [buf.buffer]))); + }, + async decompression(file) { + const fileClone = bClone(file); + const data = await workers.zlib.deflate(fileClone, [fileClone.buffer]); + const dProm = workers.fflate.inflate(data, [data.buffer]); + dProm.timeout(5000); + assert.ok(file.equals(await dProm)); + } +});
\ No newline at end of file diff --git a/test/1-size.ts b/test/1-size.ts new file mode 100644 index 0000000..c2cc247 --- /dev/null +++ b/test/1-size.ts @@ -0,0 +1,24 @@ +import { testSuites, workers, bClone } from './util'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; +import { performance } from 'perf_hooks'; +import * as assert from 'uvu/assert'; + +const sizePerf: Record<string, Record<string, [number, number]>> = {}; + +testSuites({ + async main(file, name) { + sizePerf[name] = {}; + for (const lib of (['fflate', 'pako', 'uzip', 'zlib'] as const)) { + const clone = bClone(file); + const ts = performance.now(); + sizePerf[name][lib] = [(await workers[lib].deflate([clone, { level: 9 }], [clone.buffer])).length, performance.now() - ts]; + } + for (const lib of ['pako', 'uzip', 'zlib']) { + // Less than 5% larger + assert.ok(((sizePerf[name].fflate[0] - sizePerf[name][lib][0]) / sizePerf[name][lib][0]) < 0.05); + } + } +}).then(() => { + writeFileSync(join(__dirname, 'results', 'longTimings.json'), JSON.stringify(sizePerf, null, 2)); +})
\ No newline at end of file diff --git a/test/2-perf.ts b/test/2-perf.ts new file mode 100644 index 0000000..98e0668 --- /dev/null +++ b/test/2-perf.ts @@ -0,0 +1,47 @@ +import { testSuites, workers, bClone, TestHandler } from './util'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; + +const preprocessors = { + inflate: workers.zlib.deflate, + gunzip: workers.zlib.gzip, + unzlib: workers.zlib.zlib +}; + +const cache: Record<string, Record<string, Buffer>> = { + deflate: {}, + inflate: {}, + gzip: {}, + gunzip: {}, + zlib: {}, + unzlib: {} +}; + +const flattenedWorkers: Record<string, TestHandler> = {}; +for (const k in workers) { + for (const l in workers[k]) { + if (l == 'zip' || l == 'unzip') continue; + flattenedWorkers[k + '.' + l] = async (file, name, resetTimer) => { + const fileClone = bClone(file); + let buf = fileClone; + if (preprocessors[l]) { + buf = bClone(cache[l][name] ||= Buffer.from( + await preprocessors[l as keyof typeof preprocessors](buf, [buf.buffer]) + )); + resetTimer(); + } + const opt2 = preprocessors[l] + ? k === 'tinyInflate' + ? new Uint8Array(file.length) + : null + : { level: 1 }; + await workers[k][l]([buf, opt2], opt2 instanceof Uint8Array + ? [buf.buffer, opt2.buffer] + : [buf.buffer]); + } + } +} + +testSuites(flattenedWorkers).then(perf => { + writeFileSync(join(__dirname, 'results', 'timings.json'), JSON.stringify(perf, null, 2)); +});
\ No newline at end of file diff --git a/test/3-zip.ts b/test/3-zip.ts new file mode 100644 index 0000000..840a999 --- /dev/null +++ b/test/3-zip.ts @@ -0,0 +1 @@ +// TODO: test ZIP
\ No newline at end of file diff --git a/test/4-streams.ts b/test/4-streams.ts new file mode 100644 index 0000000..c85887d --- /dev/null +++ b/test/4-streams.ts @@ -0,0 +1 @@ +// TODO: test all streams (including ZIP)
\ No newline at end of file diff --git a/test/5-async.ts b/test/5-async.ts new file mode 100644 index 0000000..c3c6c42 --- /dev/null +++ b/test/5-async.ts @@ -0,0 +1 @@ +// TODO: test all async operations (including streams and ZIP)
\ No newline at end of file diff --git a/test/data/.gitignore b/test/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/test/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore
\ No newline at end of file diff --git a/test/results/.gitignore b/test/results/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/test/results/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore
\ No newline at end of file diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..dd952bf --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node" + }, + "include": ["./*.ts"] +}
\ No newline at end of file diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 0000000..e91201f --- /dev/null +++ b/test/util.ts @@ -0,0 +1,196 @@ +import { existsSync, readFile, writeFile } from 'fs'; +import { resolve } from 'path'; +import { get } from 'https'; +import { suite } from 'uvu'; +import { performance } from 'perf_hooks'; +import { Worker } from 'worker_threads'; + +const testFiles = { + basic: Buffer.from('Hello world!'), + text: 'https://www.gutenberg.org/files/2701/old/moby10b.txt', + smallImage: 'https://hlevkin.com/hlevkin/TestImages/new/Rainier.bmp', + image: 'https://www.hlevkin.com/hlevkin/TestImages/new/Maltese.bmp', + largeImage: 'https://www.hlevkin.com/hlevkin/TestImages/new/Sunrise.bmp' +}; + +const testZipFiles = { + model3D: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/kmz/Box.kmz', + largeModel3D: 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/models/3mf/truck.3mf' +}; + +const dlCached = async <T extends Record<string, string | Buffer>>(files: T) => { + let res = {} as Record<keyof T, Buffer>; + for (const name in files) { + let data: string | Buffer = files[name]; + if (typeof data == 'string') { + const fn = resolve(__dirname, 'data', name); + if (!existsSync(fn)) { + console.log('\nDownloading ' + data + '...'); + data = await new Promise((r, re) => get(data as string, res => { + const len = +res.headers['content-length']; + const buf = Buffer.allocUnsafe(len); + let i = 0; + res.on('data', chunk => { + buf.set(chunk, i); + console.log((100 * (i += chunk.length) / len).toFixed(1) + '%\x1B[1A'); + }); + res.on('error', re); + res.on('end', () => { + console.log('Complete'); + writeFile(fn, buf, () => r(buf)); + }); + })); + } else { + data = await new Promise((res, rej) => + readFile(fn, (err, buf) => err ? rej(err) : res(buf)) + ); + } + } + res[name as keyof T] = data as Buffer; + } + return res; +} + +const testFilesPromise = dlCached(testFiles); +const testZipFilesPromise = dlCached(testZipFiles); + +export type TestHandler = (file: Buffer, name: string, resetTimer: () => void) => unknown | Promise<unknown>; + +export const testSuites = async <T extends Record<string, TestHandler>, D extends 'zip' | 'default' = 'default'>(suites: T, type?: D) => { + type DK = keyof (D extends 'zip' ? typeof testZipFiles : typeof testFiles); + const tf = type == 'zip' ? testZipFiles : testFiles; + const tfp = type == 'zip' ? testZipFilesPromise : testFilesPromise; + const perf = {} as Record<keyof T, Promise<Record<DK, number>>>; + for (const k in suites) { + perf[k] = new Promise(async setPerf => { + const ste = suite(k); + let localTestFiles: Record<DK, Buffer>; + ste.before(async () => { + localTestFiles = (await tfp) as unknown as Record<DK, Buffer>; + }); + const localPerf = {} as Record<DK, number>; + for (const name in tf) { + ste(name, async () => { + let ts = performance.now(); + await suites[k](localTestFiles[name], name, () => { + ts = performance.now(); + }); + localPerf[name] = performance.now() - ts; + }); + } + ste.after(() => { + setPerf(localPerf); + }); + ste.run(); + }) + } + const resolvedPerf = {} as Record<keyof T, Record<DK, number>>; + for (const k in suites) resolvedPerf[k] = await perf[k]; + return resolvedPerf; +}; + +export const stream = (src: Uint8Array, dst: { + push(dat: Uint8Array, final: boolean): void; +}) => { + for (let i = 0; i < src.length;) { + const off = Math.floor(Math.random() * Math.min(131072, src.length >>> 3)); + dst.push(src.slice(i, i + off), (i += off) >= src.length); + } +} + +// create worker string +const cws = (pkg: string, method: string = '_cjsDefault') => ` + const ${method == '_cjsDefault' ? method : `{ ${method} }`} = require('${pkg}'); + const { Worker, workerData, parentPort } = require('worker_threads'); + try { + const buf = ${method}(...(Array.isArray(workerData) ? workerData : [workerData])); + parentPort.postMessage(buf, [buf.buffer]); + } catch (err) { + parentPort.postMessage({ err }); + } +`; + +export type Workerized = (workerData: Uint8Array | [Uint8Array, {}], transferable?: ArrayBuffer[]) => WorkerizedResult; +export interface WorkerizedResult extends PromiseLike<Uint8Array> { + timeout(ms: number): void; +}; + +// Worker creator +const wc = (pkg: string, method?: string): Workerized => { + const str = cws(pkg, method); + return (workerData, transferable) => { + const worker = new Worker(str, { + eval: true, + workerData, + transferList: transferable + }); + let terminated = false; + return { + timeout(ms: number) { + const tm = setTimeout(() => { + worker.terminate(); + terminated = true; + }, ms); + worker.once('message', () => clearTimeout(tm)); + }, + then(res, rej) { + return new Promise((res, rej) => { + worker + .once('message', msg => { + if (msg.err) rej(msg.err); + res(msg); + }) + .once('error', rej) + .once('exit', code => { + if (terminated) rej(new Error('Timed out')); + else if (code !== 0) rej(new Error('Exited with status code ' + code)); + }); + }).then(res, rej); + } + }; + } +} + +const fflate = resolve(__dirname, '..'); + +export const workers = { + fflate: { + deflate: wc(fflate, 'deflateSync'), + inflate: wc(fflate, 'inflateSync'), + gzip: wc(fflate, 'gzipSync'), + gunzip: wc(fflate, 'gunzipSync'), + zlib: wc(fflate, 'zlibSync'), + unzlib: wc(fflate, 'unzlibSync'), + zip: wc(fflate, 'zipSync'), + unzip: wc(fflate, 'unzipSync') + }, + pako: { + deflate: wc('pako', 'deflateRaw'), + inflate: wc('pako', 'inflateRaw'), + gzip: wc('pako', 'gzip'), + gunzip: wc('pako', 'ungzip'), + zlib: wc('pako', 'deflate'), + unzlib: wc('pako', 'inflate') + }, + uzip: { + deflate: wc('uzip', 'deflateRaw'), + inflate: wc('uzip', 'inflateRaw') + }, + tinyInflate: { + inflate: wc('tiny-inflate') + }, + zlib: { + deflate: wc('zlib', 'deflateRawSync'), + inflate: wc('zlib', 'inflateRawSync'), + gzip: wc('zlib', 'gzipSync'), + gunzip: wc('zlib', 'gunzipSync'), + zlib: wc('zlib', 'deflateSync'), + unzlib: wc('zlib', 'inflateSync') + } +}; + +export const bClone = (buf: Buffer) => { + const clone = Buffer.allocUnsafe(buf.length); + clone.set(buf); + return clone; +}
\ No newline at end of file |