Skip to content

Commit 08bd8fb

Browse files
committed
begin of major refactoring:
- benchmarks added - encoding perf doubled by avoiding re-eval of pixels for every color - allow skipping ED calc with `safePalette`
1 parent 7608bb4 commit 08bd8fb

9 files changed

+391
-68
lines changed

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/dist/
22
/lib/
33
/node_modules/
4-
/package-lock.json
4+
/package-lock.json
5+
/coverage
6+
/.nyc_output
7+
/benchmark

.npmignore

+8
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,11 @@ node_example*
55
index.html
66
sixel_textcursor.sh
77
webpack.config.js
8+
coverage
9+
.nyc_output
10+
.nycrc
11+
benchmark
12+
lib/*.benchmark.*
13+
lib/*.test.*
14+
src/*.benchmark.*
15+
src/*.test.*

.nycrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "@istanbuljs/nyc-config-typescript"
3+
}

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@ of the npm package, clone the repo and run `npm install` if you want to see them
9797

9898
Decoding can also be tested in the browser after `npm start` under `localhost:8080`.
9999

100+
## Benchmarks
101+
Performance is measured for typical actions based on 9-bit palette image:
102+
![test image](palette.png "test image")
103+
104+
The test image repeats the palette image 8 times to form a 640x480 image with 512 colors. The unusual (and not spec conform) high number of colors was choosen to explicit test for this as an upper bound.
105+
106+
Results:
107+
```
108+
Context "lib/index.benchmark.js"
109+
Context "SixelImage"
110+
Case "fromImageData - unsafe palette" : 10 runs - average runtime: 21.61 ms
111+
Case "fromImageData - safe palette" : 10 runs - average runtime: 2.15 ms
112+
Case "toImageData - with fillColor" : 10 runs - average runtime: 2.97 ms
113+
Case "toImageData - without fillColor" : 10 runs - average runtime: 1.34 ms
114+
Case "writeString" : 10 runs - average runtime: 39.39 ms
115+
Case "write" : 10 runs - average runtime: 34.61 ms
116+
Case "toSixelString" : 10 runs - average runtime: 77.32 ms
117+
Case "toSixelBytes" : 10 runs - average runtime: 68.91 ms
118+
```
119+
120+
Note the high values for:
121+
- "fromImageData - unsafe palette"
122+
Caused by the additional needed ED calculation and color palette replacement. Prolly can be made faster.
123+
- "write"
124+
The decode parser is not yet further optimized.
125+
- "toSixelBytes"
126+
Encoding shows a really bad growing behavior for dimension and colors and needs a major refactoring.
127+
100128
### Status
101129
Currently alpha, tests are yet to come.
102130

package.json

+14-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sixel",
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"description": "Sixel image format for node and browser.",
55
"main": "./lib/index.js",
66
"scripts": {
@@ -9,7 +9,9 @@
99
"watch": "tsc -w",
1010
"lint": "tslint 'src/**/*.ts'",
1111
"start": "webpack-cli && http-server",
12-
"prepublish": "npm run tsc"
12+
"prepublish": "npm run tsc",
13+
"coverage": "nyc --reporter=lcov --reporter=text --reporter=html npm test",
14+
"benchmark": "xterm-benchmark $*"
1315
},
1416
"keywords": [
1517
"sixel",
@@ -23,22 +25,27 @@
2325
"author": "Joerg Breitbart <[email protected]>",
2426
"license": "MIT",
2527
"dependencies": {
26-
"@types/node": "^10.12.18"
28+
"@types/node": "^10.14.21"
2729
},
2830
"devDependencies": {
31+
"@istanbuljs/nyc-config-typescript": "^0.1.3",
2932
"@types/chai": "^4.2.3",
3033
"@types/mocha": "^5.2.7",
3134
"canvas": "^2.6.0",
3235
"chai": "^4.2.0",
3336
"http-server": "^0.11.1",
3437
"mocha": "^5.2.0",
3538
"node-ansiparser": "^2.2.0",
39+
"nyc": "^14.1.1",
3640
"open": "^6.4.0",
3741
"rgbquant": "^1.1.2",
38-
"ts-loader": "^5.3.3",
42+
"source-map-support": "^0.5.13",
43+
"ts-loader": "^5.4.5",
44+
"ts-node": "^8.4.1",
3945
"tslint": "^5.20.0",
40-
"typescript": "^3.2.4",
41-
"webpack": "^4.29.0",
42-
"webpack-cli": "^3.2.1"
46+
"typescript": "^3.6.4",
47+
"webpack": "^4.41.1",
48+
"webpack-cli": "^3.3.9",
49+
"xterm-benchmark": "^0.1.3"
4350
}
4451
}

palette.png

525 Bytes
Loading

src/index.benchmark.ts

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { RuntimeCase, perfContext, beforeEach } from 'xterm-benchmark';
2+
import { toRGBA8888, SixelImage, RGBA8888, fromRGBA8888 } from './index';
3+
4+
5+
// test data: 9-bit palette in 10x10 tiles (512 colors: 8*8*8) - 640x80 -> 6 rows => 640x480
6+
const {SOURCE32, SOURCE8, PALETTE, TEST_IMAGE, SIXELSTRING, SIXELBYTES} = (() => {
7+
const channelValues = Array.from(Array(8).keys()).map(v => v * 32);
8+
const palette: RGBA8888[] = [];
9+
for (let r = 0; r < channelValues.length; ++r) {
10+
for (let g = 0; g < channelValues.length; ++g) {
11+
for (let b = 0; b < channelValues.length; ++b) {
12+
palette.push(toRGBA8888(channelValues[r], channelValues[g], channelValues[b]));
13+
}
14+
}
15+
}
16+
const source32 = new Uint32Array(512 * 10 * 10 * 6);
17+
for (let row = 0; row < 6; ++row) {
18+
for (let colorIdx = 0; colorIdx < 512; ++colorIdx) {
19+
const cy = colorIdx % 8;
20+
const cx = Math.floor(colorIdx / 8);
21+
for (let y = 0; y < 10; ++y) {
22+
for (let x = 0; x < 10; ++x) {
23+
source32[row * 640 * 80 + cy * 8 * 8 * 10 * 10 + y * 8 * 8 * 10 + cx * 10 + x] = palette[colorIdx];
24+
}
25+
}
26+
}
27+
}
28+
const source8 = new Uint8Array(source32.buffer);
29+
const testImage = SixelImage.fromImageData(source8, 640, 480, palette);
30+
const sixelString = testImage.toSixelString();
31+
const bytes = new Uint8Array(sixelString.length);
32+
for (let i = 0; i < sixelString.length; ++i) bytes[i] = sixelString.charCodeAt(i);
33+
return {
34+
SOURCE32: source32,
35+
SOURCE8: source8,
36+
PALETTE: palette,
37+
TEST_IMAGE: testImage,
38+
SIXELSTRING: sixelString,
39+
SIXELBYTES: bytes
40+
};
41+
})();
42+
const TARGET = new Uint8ClampedArray(512 * 10 * 10 * 6 * 4);
43+
44+
// preview test image
45+
function preview(sixelImage: SixelImage): void {
46+
const { createCanvas, createImageData } = require('canvas');
47+
const fs = require('fs');
48+
const open = require('open');
49+
const width = sixelImage.width;
50+
const height = sixelImage.height;
51+
const imageData = createImageData(width, height);
52+
sixelImage.toImageData(imageData.data, width, height);
53+
const canvas = createCanvas(width, height);
54+
const ctx = canvas.getContext('2d');
55+
ctx.putImageData(imageData, 0, 0);
56+
const targetFile = __dirname + '/testimage.png';
57+
const out = fs.createWriteStream(targetFile);
58+
const stream = canvas.createPNGStream();
59+
stream.pipe(out);
60+
out.on('finish', () => open(targetFile));
61+
}
62+
63+
function previewTerminal(sixelImage: SixelImage): void {
64+
console.log(SixelImage.introducer(1));
65+
console.log(sixelImage.toSixelString());
66+
console.log(SixelImage.finalizer());
67+
}
68+
//preview(TEST_IMAGE);
69+
//previewTerminal(TEST_IMAGE);
70+
//console.log(TEST_IMAGE.toSixelString().length);
71+
72+
73+
perfContext('SixelImage', () => {
74+
new RuntimeCase('fromImageData - unsafe palette', () => {
75+
const img = SixelImage.fromImageData(SOURCE8, 640, 480, PALETTE, false);
76+
return img.width;
77+
}, {repeat: 10}).showAverageRuntime();
78+
new RuntimeCase('fromImageData - safe palette', () => {
79+
const img = SixelImage.fromImageData(SOURCE8, 640, 480, PALETTE, true);
80+
return img.width;
81+
}, {repeat: 10}).showAverageRuntime();
82+
83+
new RuntimeCase('toImageData - with fillColor', () => {
84+
return TEST_IMAGE.toImageData(TARGET, 640, 480, 0, 0, 0, 0, 640, 480, toRGBA8888(0, 0, 0));
85+
}, {repeat: 10}).showAverageRuntime();
86+
new RuntimeCase('toImageData - without fillColor', () => {
87+
return TEST_IMAGE.toImageData(TARGET, 640, 480, 0, 0, 0, 0, 640, 480, 0);
88+
}, {repeat: 10}).showAverageRuntime();
89+
90+
new RuntimeCase('writeString', () => {
91+
const img = new SixelImage();
92+
img.writeString(SIXELSTRING);
93+
return img.width;
94+
}, {repeat: 10}).showAverageRuntime();
95+
new RuntimeCase('write', () => {
96+
const img = new SixelImage();
97+
img.write(SIXELBYTES);
98+
return img.width;
99+
}, {repeat: 10}).showAverageRuntime();
100+
101+
new RuntimeCase('toSixelString', () => {
102+
return TEST_IMAGE.toSixelString().length;
103+
}, {repeat: 10}).showAverageRuntime();
104+
new RuntimeCase('toSixelBytes', () => {
105+
let length = 0;
106+
TEST_IMAGE.toSixelBytes(c => { length += c.length; });
107+
return length;
108+
}, {repeat: 10}).showAverageRuntime();
109+
// }, {repeat: 1, fork: true, forkOptions: {execArgv: ['--inspect-brk']}}).showAverageRuntime();
110+
});

src/index.test.ts

+112-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert } from 'chai';
2-
import { SixelImage, fromRGBA8888, toRGBA8888 } from './index';
2+
import { SixelImage, fromRGBA8888, toRGBA8888, RGBA8888 } from './index';
33

44
const BIG_ENDIAN = new Uint8Array(new Uint32Array([0xFF000000]).buffer)[0] === 0xFF;
55

@@ -57,6 +57,16 @@ describe('RGBA8888 native colors', () => {
5757
});
5858
})
5959

60+
class ChunkWriter {
61+
public pos = 0;
62+
constructor(public target: Uint8Array) {}
63+
public write(chunk: Uint8Array): number {
64+
this.target.set(chunk, this.pos);
65+
this.pos += chunk.length;
66+
return this.pos;
67+
}
68+
}
69+
6070
describe('SixelImage', () => {
6171
let img: SixelImage;
6272
beforeEach(() => {
@@ -223,4 +233,105 @@ describe('SixelImage', () => {
223233
});
224234
});
225235
});
236+
describe('encode - decode', () => {
237+
let source32: Uint32Array;
238+
let source8: Uint8ClampedArray;
239+
let target32: Uint32Array;
240+
let target8: Uint8ClampedArray;
241+
let sixels: Uint8Array;
242+
let writer: ChunkWriter;
243+
beforeEach(() => {
244+
// test with max 100x100 pixel data
245+
source32 = new Uint32Array(100 * 100);
246+
source8 = new Uint8ClampedArray(source32.buffer);
247+
target32 = new Uint32Array(100 * 100);
248+
target8 = new Uint8ClampedArray(target32.buffer);
249+
sixels = new Uint8Array(1000000); // hard to precalc
250+
writer = new ChunkWriter(sixels);
251+
});
252+
it('10x1 black', () => {
253+
// prepare data
254+
for (let i = 0; i < 10; ++i) source32[i] = toRGBA8888(0, 0, 0);
255+
// encode
256+
const imgEnc = SixelImage.fromImageData(source8.subarray(0, 10 * 4), 10, 1, [toRGBA8888(0, 0, 0)]);
257+
imgEnc.toSixelBytes(chunk => writer.write(chunk));
258+
// decode
259+
const imgDec = new SixelImage(0);
260+
imgDec.write(sixels, 0, writer.pos);
261+
imgDec.toImageData(target8.subarray(0, 10 * 4), 10, 1);
262+
// compare
263+
assert.equal(imgEnc.toSixelString(), imgDec.toSixelString());
264+
assert.deepEqual(target8, source8);
265+
assert.equal(imgEnc.width, 10);
266+
assert.equal(imgEnc.height, 1);
267+
assert.equal(imgDec.width, 10);
268+
assert.equal(imgDec.height, 1);
269+
});
270+
it('10x1 with 8 colors', () => {
271+
// prepare data
272+
const palette: RGBA8888[] = [
273+
toRGBA8888(0, 0, 0),
274+
toRGBA8888(255, 0, 0),
275+
toRGBA8888(0, 255, 0),
276+
toRGBA8888(0, 0, 255),
277+
toRGBA8888(255, 255, 0),
278+
toRGBA8888(255, 0, 255),
279+
toRGBA8888(0, 255, 255),
280+
toRGBA8888(255, 255, 255)
281+
];
282+
for (let i = 0; i < 8; ++i) source32[i] = palette[i];
283+
// encode
284+
const imgEnc = SixelImage.fromImageData(source8.subarray(0, 8 * 4), 8, 1, palette);
285+
imgEnc.toSixelBytes(chunk => writer.write(chunk));
286+
// decode
287+
const imgDec = new SixelImage(0);
288+
imgDec.write(sixels, 0, writer.pos);
289+
imgDec.toImageData(target8.subarray(0, 8 * 4), 8, 1);
290+
// compare
291+
assert.equal(imgEnc.toSixelString(), imgDec.toSixelString());
292+
assert.deepEqual(target8, source8);
293+
assert.equal(imgEnc.width, 8);
294+
assert.equal(imgEnc.height, 1);
295+
assert.equal(imgDec.width, 8);
296+
assert.equal(imgDec.height, 1);
297+
});
298+
it('100x100 with 256 random colors (noise)', () => {
299+
// prepare data
300+
// generate 256 random colors
301+
const strippedPal: number[] = [];
302+
while (strippedPal.length < 256) {
303+
const v = Math.floor(Math.random() * (255 << 16 | 255 << 8 | 255));
304+
if (!~strippedPal.indexOf(v)) strippedPal.push(v);
305+
}
306+
// convert to sixel palette
307+
const palette: RGBA8888[] = [];
308+
for (let i = 0; i < 256; ++i) {
309+
const v = strippedPal[i];
310+
// we have to do a normalization to 100 steps in between
311+
// channels values between cannot be expressed in SIXEL (lower color resolution)
312+
const r = Math.round(Math.round((v >> 16) / 255 * 100) / 100 * 255);
313+
const g = Math.round(Math.round(((v >> 8) & 0xFF) / 255 * 100) / 100 * 255);
314+
const b = Math.round(Math.round((v & 0xFF) / 255 * 100) / 100 * 255);
315+
palette.push(toRGBA8888(r, g, b));
316+
}
317+
// apply to input data
318+
for (let i = 0; i < 100 * 100; ++i) {
319+
source32[i] = palette[Math.floor(Math.random() * 256)];
320+
}
321+
// encode
322+
const imgEnc = SixelImage.fromImageData(source8, 100, 100, palette);
323+
imgEnc.toSixelBytes(chunk => writer.write(chunk));
324+
// decode
325+
const imgDec = new SixelImage(0);
326+
imgDec.write(sixels, 0, writer.pos);
327+
imgDec.toImageData(target8, 100, 100);
328+
// compare
329+
assert.equal(imgEnc.toSixelString(), imgDec.toSixelString());
330+
assert.deepEqual(target8, source8);
331+
assert.equal(imgEnc.width, 100);
332+
assert.equal(imgEnc.height, 100);
333+
assert.equal(imgDec.width, 100);
334+
assert.equal(imgDec.height, 100);
335+
});
336+
});
226337
});

0 commit comments

Comments
 (0)