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/[email protected]
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: work on benchmarks, improved metadata, stream-decoder
AVVS committed Apr 30, 2024

Verified

This commit was signed with the committer’s verified signature.
AVVS Vitaly Aminev
commit 4b64cb680eb5edcf52bff4563c43a8604027a27b
1 change: 1 addition & 0 deletions packages/grpc-js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.clinic
4 changes: 4 additions & 0 deletions packages/grpc-js/benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@ This folder contains basic benchmarks to approximate performance impact of chang
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:
68 changes: 68 additions & 0 deletions packages/grpc-js/benchmarks/bench/metadata.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const { 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 suite = createBenchmarkSuite('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',
[sensitiveHeaders]: [],
},
null
);

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

suite
.add('[email protected] fromHttp2Headers', function () {
return MetadataOriginal.fromHttp2Headers(headers);
})
.add('[email protected] toHttp2Headers', function () {
return ogMeta.toHttp2Headers();
})
.add('[email protected] fromHttp2Headers + common operations', function () {
const metadata = MetadataOriginal.fromHttp2Headers(headers);
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);
})
.add('current fromHttp2Headers', function () {
return Metadata.fromHttp2Headers(headers);
})
.add('current toHttp2Headers', function () {
return currentMeta.toHttp2Headers();
})
.add('current + common operations', function () {
const metadata = Metadata.fromHttp2Headers(headers);
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);
})
.run({ async: false });
108 changes: 108 additions & 0 deletions packages/grpc-js/benchmarks/bench/stream-decoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const { 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,
} = require('../../build/src/stream-decoder');

const suite = createBenchmarkSuite('Stream Decoder');

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

const smallBinarySplitPartOne = Buffer.from(smallBinary.subarray(0, 3));
const smallBinarySplitPartTwo = Buffer.from(smallBinary.subarray(3, 5));
const smallBinarySplitPartThree = Buffer.from(smallBinary.subarray(5));

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

const largeBinarySplitPartOne = Buffer.from(largeBinary.subarray(0, 4096));
const largeBinarySplitPartTwo = Buffer.from(largeBinary.subarray(4096));

const cachedSD2 = new NewStreamDecoder();
const cachedOG = new OGStreamDecoder();

suite
.add('original stream decoder', function () {
const decoder = new OGStreamDecoder();
decoder.write(smallBinary);
})
.add('original stream decoder cached', function () {
cachedOG.write(smallBinary);
})
.add('stream decoder v2', function () {
const decoder = new NewStreamDecoder();
decoder.write(smallBinary);
})
.add('stream decoder v2 cached', function () {
cachedSD2.write(smallBinary);
})
.add('original stream decoder - large', function () {
const decoder = new OGStreamDecoder();
decoder.write(largeBinary);
})
.add('original stream decoder cached - large', function () {
cachedOG.write(largeBinary);
})
.add('stream decoder v2 - large', function () {
const decoder = new NewStreamDecoder();
decoder.write(largeBinary);
})
.add('stream decoder v2 cached - large', function () {
cachedSD2.write(largeBinary);
})
.add('original stream decoder - small split', function () {
const decoder = new OGStreamDecoder();
decoder.write(smallBinarySplitPartOne);
decoder.write(smallBinarySplitPartTwo);
decoder.write(smallBinarySplitPartThree);
})
.add('original stream decoder cached - small split', function () {
cachedOG.write(smallBinarySplitPartOne);
cachedOG.write(smallBinarySplitPartTwo);
cachedOG.write(smallBinarySplitPartThree);
})
.add('stream decoder v2 - small split', function () {
const decoder = new NewStreamDecoder();
decoder.write(smallBinarySplitPartOne);
decoder.write(smallBinarySplitPartTwo);
decoder.write(smallBinarySplitPartThree);
})
.add('stream decoder v2 cached - small split', function () {
cachedSD2.write(smallBinarySplitPartOne);
cachedSD2.write(smallBinarySplitPartTwo);
cachedSD2.write(smallBinarySplitPartThree);
})
.add('original stream decoder - large split', function () {
const decoder = new OGStreamDecoder();
decoder.write(largeBinarySplitPartOne);
decoder.write(largeBinarySplitPartTwo);
})
.add('original stream decoder cached - large split', function () {
cachedOG.write(largeBinarySplitPartOne);
cachedOG.write(largeBinarySplitPartTwo);
})
.add('stream decoder v2 - large split', function () {
const decoder = new NewStreamDecoder();
decoder.write(largeBinarySplitPartOne);
decoder.write(largeBinarySplitPartTwo);
})
.add('stream decoder v2 cached - large split', function () {
cachedSD2.write(largeBinarySplitPartOne);
cachedSD2.write(largeBinarySplitPartTwo);
})
.run({ async: false });
100 changes: 100 additions & 0 deletions packages/grpc-js/benchmarks/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const Benchmark = require('benchmark');
const { createTableHeader, H2, eventToMdTable } = require('./markdown');
const os = require('os');

function installMarkdownEmitter(
suite,
name,
tableHeaderColumns = ['name', 'ops/sec', 'samples']
) {
const tableHeader = createTableHeader(tableHeaderColumns);

suite
.on('start', function () {
console.log(H2(name));
console.log(tableHeader);
})
.on('cycle', function (event) {
console.log(eventToMdTable(event));
});
}

function getMachineInfo() {
return {
platform: os.platform(),
arch: os.arch(),
cpus: os.cpus().length,
totalMemory: os.totalmem() / 1024 ** 3,
};
}

function installMarkdownMachineInfo(suite) {
if (!process.env.CI) return;

const { platform, arch, cpus, totalMemory } = getMachineInfo();

const machineInfo = `${platform} ${arch} | ${cpus} vCPUs | ${totalMemory.toFixed(
1
)}GB Mem`;

suite.on('complete', () => {
const writter = process.stdout;

writter.write('\n\n');
writter.write('<details>\n');
writter.write('<summary>Environment</summary>');
writter.write(`\n
* __Machine:__ ${machineInfo}
* __Run:__ ${new Date()}
`);
writter.write('</details>');
writter.write('\n\n');
});
}

function installMarkdownHiddenDetailedInfo(suite) {
if (!process.env.CI) return;

const cycleEvents = [];

suite
.on('cycle', function (event) {
cycleEvents.push({
name: event.target.name,
opsSec: event.target.hz,
samples: event.target.cycles,
});
})
.on('complete', function () {
const writter = process.stdout;

writter.write('<!--\n');
writter.write(
JSON.stringify({
environment: getMachineInfo(),
benchmarks: cycleEvents,
})
);
writter.write('-->\n');
});
}

function createBenchmarkSuite(
name,
{ tableHeaderColumns = ['name', 'ops/sec', 'samples'] } = {}
) {
const suite = new Benchmark.Suite();

installMarkdownEmitter(suite, name, tableHeaderColumns);
installMarkdownMachineInfo(suite);
installMarkdownHiddenDetailedInfo(suite);

return suite;
}

module.exports = {
createBenchmarkSuite,
installMarkdownEmitter,
installMarkdownMachineInfo,
installMarkdownHiddenDetailedInfo,
};
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as fs from 'node:fs';
import { resolve } from 'node:path';
import { echoService } from './utils';
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: any, value: any) {
function serializeMessage(serialize, value) {
const messageBuffer = serialize(value);
const byteLength = messageBuffer.byteLength;
const output = Buffer.allocUnsafe(byteLength + 5);
@@ -27,11 +27,15 @@ const binaryMessage = serializeMessage(
}
);

console.log(
'Service %s\nEcho binary bytes: %d, hex: %s',
echoService.service.Echo.path,
binaryMessage.length,
binaryMessage.toString('hex')
);
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);
}

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;
29 changes: 0 additions & 29 deletions packages/grpc-js/benchmarks/helpers/utils.ts

This file was deleted.

Loading