Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: stream-decoder, metadata #2733

Draft
wants to merge 9 commits into
base: @grpc/grpc-js@1.10.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/grpc-js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.clinic
88 changes: 88 additions & 0 deletions packages/grpc-js/benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
This folder contains basic benchmarks to approximate performance impact of changes


## How to test

1. pnpm build; pnpm ts-node --transpile-only ./benchmarks/server.ts
2. pnpm tsc -p tsconfig.modern.json; pnpm ts-node --transpile-only ./benchmarks/server.ts
3. ideally run with jemalloc or memory fragmentation makes everything run slower over time

For mac os:
`DYLD_INSERT_LIBRARIES=$(brew --prefix jemalloc)/lib/libjemalloc.dylib pnpm ts-node --transpile-only ./benchmarks/server.ts`

`DYLD_INSERT_LIBRARIES=$(brew --prefix jemalloc)/lib/libjemalloc.dylib NODE_ENV=production clinic flame -- node -r ts-node/register/transpile-only ./benchmarks/server.ts`

`DYLD_INSERT_LIBRARIES=$(brew --prefix jemalloc)/lib/libjemalloc.dylib NODE_ENV=production node -r ts-node/register/transpile-only --trace-opt --trace-deopt ./benchmarks/server.ts`

2. h2load -n200000 -m 50 http://localhost:9999/EchoService/Echo -c10 -t 10 -H 'content-type: application/grpc' -d ./echo-unary.bin

Baseline on M1 Max Laptop:

```
ES2017 & ESNext targets are within margin of error:

finished in 4.09s, 48851.86 req/s, 2.47MB/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 10.11MB (10603710) total, 978.38KB (1001860) headers (space savings 96.40%), 3.62MB (3800000) data
min max mean sd +/- sd
time for request: 2.47ms 104.19ms 10.18ms 3.46ms 94.40%
time for connect: 790us 1.13ms 879us 98us 90.00%
time to 1st byte: 12.09ms 97.17ms 52.68ms 28.04ms 60.00%
req/s : 4885.61 4922.01 4901.67 14.07 50.00%


```

---

Changes to stream decoder:

1. switch -> if

```
h2load -n200000 -m 50 http://localhost:9999/EchoService/Echo -c10 -t 10 -H 'content-type: application/grpc' -d ./echo-unary.bin

finished in 3.82s, 52410.67 req/s, 2.65MB/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 10.11MB (10603690) total, 978.36KB (1001840) headers (space savings 96.40%), 3.62MB (3800000) data
min max mean sd +/- sd
time for request: 1.87ms 47.64ms 9.49ms 1.89ms 97.25%
time for connect: 1.75ms 3.14ms 2.43ms 410us 70.00%
time to 1st byte: 6.58ms 45.08ms 23.01ms 13.70ms 60.00%
req/s : 5242.32 5270.74 5253.00 9.37 70.00%
```

2. const enum is comparable to enum

3. fewer buffer.concat,f unsafeAlloc


```
finished in 3.40s, 58763.66 req/s, 987.33KB/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 3.28MB (3441011) total, 1.01MB (1063183) headers (space savings 97.04%), 176.74KB (180986) data
min max mean sd +/- sd
time for request: 304us 41.57ms 3.28ms 1.63ms 80.98%
time for connect: 831us 1.47ms 1.14ms 181us 70.00%
time to 1st byte: 2.64ms 25.10ms 11.42ms 7.87ms 60.00%
req/s : 5877.32 6303.71 6082.75 168.23 50.00%
```


```
old decoder:

finished in 3.83s, 52210.19 req/s, 2.64MB/s
requests: 200000 total, 200000 started, 200000 done, 200000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 200000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 10.11MB (10603670) total, 978.34KB (1001820) headers (space savings 96.40%), 3.62MB (3800000) data
min max mean sd +/- sd
time for request: 1.16ms 18.75ms 3.82ms 1.45ms 88.89%
time for connect: 723us 1.38ms 1.18ms 191us 80.00%
time to 1st byte: 3.45ms 17.72ms 9.00ms 4.95ms 70.00%
req/s : 5221.65 5235.13 5225.05 4.23 90.00%
```

75 changes: 75 additions & 0 deletions packages/grpc-js/benchmarks/bench/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { benchmark, createBenchmarkSuite } = require('../common');
const {
sensitiveHeaders,
constants: {
HTTP2_HEADER_ACCEPT_ENCODING,
HTTP2_HEADER_TE,
HTTP2_HEADER_CONTENT_TYPE,
},
} = require('node:http2');
const {
Metadata: MetadataOriginal,
} = require('@grpc/grpc-js/build/src/metadata');
const { Metadata } = require('../../build/src/metadata');

const GRPC_ACCEPT_ENCODING_HEADER = 'grpc-accept-encoding';
const GRPC_ENCODING_HEADER = 'grpc-encoding';
const GRPC_TIMEOUT_HEADER = 'grpc-timeout';
const headers = Object.setPrototypeOf(
{
':path': '/EchoService/Echo',
':scheme': 'http',
':authority': 'localhost:9999',
':method': 'POST',
'user-agent': 'h2load nghttp2/1.58.0',
'content-type': 'application/grpc',
'content-length': '19',
[GRPC_ACCEPT_ENCODING_HEADER]: 'identity,deflate,gzip',
[GRPC_ENCODING_HEADER]: 'identity',
[sensitiveHeaders]: [],
},
null
);

const ogMeta = MetadataOriginal.fromHttp2Headers(headers);
const currentMeta = Metadata.fromHttp2Headers(headers);

const removeHeaders = metadata => {
metadata.remove(GRPC_TIMEOUT_HEADER);
metadata.remove(GRPC_ENCODING_HEADER);
metadata.remove(GRPC_ACCEPT_ENCODING_HEADER);
metadata.remove(HTTP2_HEADER_ACCEPT_ENCODING);
metadata.remove(HTTP2_HEADER_TE);
metadata.remove(HTTP2_HEADER_CONTENT_TYPE);
};

removeHeaders(ogMeta);
removeHeaders(currentMeta);

createBenchmarkSuite('fromHttp2Headers')
.add('1.10.6', function () {
MetadataOriginal.fromHttp2Headers(headers);
})
.add('current', function () {
Metadata.fromHttp2Headers(headers);
});

createBenchmarkSuite('fromHttp2Headers + common operations')
.add('1.10.6', () => {
const metadata = MetadataOriginal.fromHttp2Headers(headers);
removeHeaders(metadata);
})
.add('current', () => {
const metadata = Metadata.fromHttp2Headers(headers);
removeHeaders(metadata);
});

createBenchmarkSuite('toHttp2Headers')
.add('1.10.6', function () {
return ogMeta.toHttp2Headers();
})
.add('current', function () {
return currentMeta.toHttp2Headers();
});

benchmark.run();
138 changes: 138 additions & 0 deletions packages/grpc-js/benchmarks/bench/stream-decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
const { benchmark, createBenchmarkSuite } = require('../common');
const { serializeMessage } = require('../helpers/encode');
const { echoService } = require('../helpers/utils');
const {
StreamDecoder: OGStreamDecoder,
} = require('@grpc/grpc-js/build/src/stream-decoder');
const {
StreamDecoder: NewStreamDecoder,
decoder: decoderManager,
} = require('../../build/src/stream-decoder');

const serializedSmallBinary = serializeMessage(
echoService.service.Echo.requestSerialize,
{
value: 'string-val',
value2: 10,
}
);
const getSmallBinary = () => {
const buf = Buffer.allocUnsafe(serializedSmallBinary.length);
serializedSmallBinary.copy(buf);
return buf;
};

const getSmallSplit = () => {
const binary = getSmallBinary();
return [binary.subarray(0, 3), binary.subarray(3, 5), binary.subarray(5)];
};

const largeObj = {
value: 'a'.repeat(2 ** 16),
value2: 12803182109,
};
const serializedLargeObj = serializeMessage(
echoService.service.Echo.requestSerialize,
largeObj
);

const getLargeBinary = () => {
const buf = Buffer.allocUnsafeSlow(serializedLargeObj.length);
serializedLargeObj.copy(buf);
return buf;
};

const getLargeSplit = () => {
const binary = getLargeBinary();
return [
binary.subarray(0, Math.ceil(Buffer.poolSize * 0.5)),
binary.subarray(Math.ceil(Buffer.poolSize * 0.5)),
];
};

const originalCached = new OGStreamDecoder();
const currentCached = decoderManager.get();

createBenchmarkSuite('Small Payload')
// mark -- original decoder, fresh copies
.add('1.10.6', function () {
const decoder = new OGStreamDecoder();
decoder.write(getSmallBinary());
})
.add('1.10.6 cached', function () {
const decoder = new OGStreamDecoder();
decoder.write(getSmallBinary());
})
.add('current', function () {
const decoder = new NewStreamDecoder();
decoder.write(getSmallBinary());
})
.add('current cached', function () {
currentCached.write(getSmallBinary());
});

createBenchmarkSuite('Small Payload Chunked')
.add('1.10.6', function () {
const decoder = new OGStreamDecoder();
for (const item of getSmallSplit()) {
decoder.write(item);
}
})
.add('1.10.6 cached', function () {
for (const item of getSmallSplit()) {
originalCached.write(item);
}
})
.add('current', function () {
const decoder = new NewStreamDecoder();
for (const item of getSmallSplit()) {
decoder.write(item);
}
})
.add('current cached', function () {
for (const item of getSmallSplit()) {
currentCached.write(item);
}
});

createBenchmarkSuite('Large Payload')
.add('1.10.6', function () {
const decoder = new OGStreamDecoder();
decoder.write(getLargeBinary());
})
.add('1.10.6 cached', function () {
originalCached.write(getLargeBinary());
})
.add('current', function () {
const decoder = new NewStreamDecoder();
decoder.write(getLargeBinary());
})
.add('current cached', function () {
currentCached.write(getLargeBinary());
});

createBenchmarkSuite('Large Payload Chunked')
.add('1.10.6', function () {
const decoder = new OGStreamDecoder();
for (const item of getLargeSplit()) {
decoder.write(item);
}
})
.add('1.10.6 cached', function () {
for (const item of getLargeSplit()) {
originalCached.write(item);
}
})
.add('current', function () {
const decoder = new NewStreamDecoder();
for (const item of getLargeSplit()) {
decoder.write(item);
}
})
.add('current cached', function () {
for (const item of getLargeSplit()) {
currentCached.write(item);
}
});

benchmark.run();
14 changes: 14 additions & 0 deletions packages/grpc-js/benchmarks/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Benchmarkify = require('benchmarkify');

const benchmark = new Benchmarkify('grpc-js benchmarks').printHeader();

function createBenchmarkSuite(name) {
const suite = benchmark.createSuite(name);

return suite;
}

module.exports = {
benchmark,
createBenchmarkSuite,
};
Binary file added packages/grpc-js/benchmarks/echo-unary.bin
Binary file not shown.
41 changes: 41 additions & 0 deletions packages/grpc-js/benchmarks/helpers/encode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const fs = require('node:fs');
const { resolve } = require('node:path');
const { echoService } = require('./utils');

/**
* Serialize a message to a length-delimited byte string.
* @param value
* @returns
*/
function serializeMessage(serialize, value) {
const messageBuffer = serialize(value);
const byteLength = messageBuffer.byteLength;
const output = Buffer.allocUnsafe(byteLength + 5);
/* Note: response compression is currently not supported, so this
* compressed bit is always 0. */
output.writeUInt8(0, 0);
output.writeUInt32BE(byteLength, 1);
messageBuffer.copy(output, 5);
return output;
}

const binaryMessage = serializeMessage(
echoService.service.Echo.requestSerialize,
{
value: 'string-val',
value2: 10,
}
);

if (require.main === module) {
console.log(
'Service %s\nEcho binary bytes: %d, hex: %s',
echoService.service.Echo.path,
binaryMessage.length,
binaryMessage.toString('hex')
);

fs.writeFileSync(resolve(__dirname, '../echo-unary.bin'), binaryMessage);
}

exports.serializeMessage = serializeMessage;
28 changes: 28 additions & 0 deletions packages/grpc-js/benchmarks/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const loader = require('@grpc/proto-loader');
const path = require('node:path');

// eslint-disable-next-line node/no-unpublished-import
const { loadPackageDefinition } = require('../../build/src/make-client');

const protoLoaderOptions = {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
};

function loadProtoFile(file) {
const packageDefinition = loader.loadSync(file, protoLoaderOptions);
return loadPackageDefinition(packageDefinition);
}

const protoFile = path.join(
__dirname,
'../../test/fixtures',
'echo_service.proto'
);
const echoService = loadProtoFile(protoFile).EchoService;

exports.loadProtoFile = loadProtoFile;
exports.echoService = echoService;