grpc-js-core: use Map for metadata store

In more recent versions of Node, Maps are more performant than
POJOs when used as maps. Switching to Maps also eliminates an
expensive delete operation, as well as uses of hasOwnProperty().
This commit is contained in:
cjihrig 2018-10-24 11:50:45 -04:00
parent 8df65a91a2
commit 6a19cf5205
No known key found for this signature in database
GPG Key ID: 7434390BDBE9B9C5
2 changed files with 38 additions and 32 deletions

View File

@ -4,19 +4,21 @@ const LEGAL_KEY_REGEX = /^[0-9a-z_.-]+$/;
const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/; const LEGAL_NON_BINARY_VALUE_REGEX = /^[ -~]*$/;
export type MetadataValue = string|Buffer; export type MetadataValue = string|Buffer;
export type MetadataObject = Map<string, MetadataValue[]>;
export interface MetadataObject { [key: string]: MetadataValue[]; }
function cloneMetadataObject(repr: MetadataObject): MetadataObject { function cloneMetadataObject(repr: MetadataObject): MetadataObject {
const result: MetadataObject = {}; const result: MetadataObject = new Map<string, MetadataValue[]>();
forOwn(repr, (value, key) => {
result[key] = value.map(v => { repr.forEach((value, key) => {
const clonedValue: MetadataValue[] = value.map(v => {
if (v instanceof Buffer) { if (v instanceof Buffer) {
return Buffer.from(v); return Buffer.from(v);
} else { } else {
return v; return v;
} }
}); });
result.set(key, clonedValue);
}); });
return result; return result;
} }
@ -64,7 +66,7 @@ function validate(key: string, value?: MetadataValue): void {
* A class for storing metadata. Keys are normalized to lowercase ASCII. * A class for storing metadata. Keys are normalized to lowercase ASCII.
*/ */
export class Metadata { export class Metadata {
protected internalRepr: MetadataObject = {}; protected internalRepr: MetadataObject = new Map<string, MetadataValue[]>();
/** /**
* Sets the given value for the given key by replacing any other values * Sets the given value for the given key by replacing any other values
@ -76,7 +78,7 @@ export class Metadata {
set(key: string, value: MetadataValue): void { set(key: string, value: MetadataValue): void {
key = normalizeKey(key); key = normalizeKey(key);
validate(key, value); validate(key, value);
this.internalRepr[key] = [value]; this.internalRepr.set(key, [value]);
} }
/** /**
@ -89,10 +91,13 @@ export class Metadata {
add(key: string, value: MetadataValue): void { add(key: string, value: MetadataValue): void {
key = normalizeKey(key); key = normalizeKey(key);
validate(key, value); validate(key, value);
if (!this.internalRepr[key]) {
this.internalRepr[key] = [value]; const existingValue: MetadataValue[]|undefined = this.internalRepr.get(key);
if (existingValue === undefined) {
this.internalRepr.set(key, [value]);
} else { } else {
this.internalRepr[key].push(value); existingValue.push(value);
} }
} }
@ -103,9 +108,7 @@ export class Metadata {
remove(key: string): void { remove(key: string): void {
key = normalizeKey(key); key = normalizeKey(key);
validate(key); validate(key);
if (Object.prototype.hasOwnProperty.call(this.internalRepr, key)) { this.internalRepr.delete(key);
delete this.internalRepr[key];
}
} }
/** /**
@ -116,11 +119,7 @@ export class Metadata {
get(key: string): MetadataValue[] { get(key: string): MetadataValue[] {
key = normalizeKey(key); key = normalizeKey(key);
validate(key); validate(key);
if (Object.prototype.hasOwnProperty.call(this.internalRepr, key)) { return this.internalRepr.get(key) || [];
return this.internalRepr[key];
} else {
return [];
}
} }
/** /**
@ -130,7 +129,8 @@ export class Metadata {
*/ */
getMap(): {[key: string]: MetadataValue} { getMap(): {[key: string]: MetadataValue} {
const result: {[key: string]: MetadataValue} = {}; const result: {[key: string]: MetadataValue} = {};
forOwn(this.internalRepr, (values, key) => {
this.internalRepr.forEach((values, key) => {
if (values.length > 0) { if (values.length > 0) {
const v = values[0]; const v = values[0];
result[key] = v instanceof Buffer ? v.slice() : v; result[key] = v instanceof Buffer ? v.slice() : v;
@ -157,8 +157,11 @@ export class Metadata {
* @param other A Metadata object. * @param other A Metadata object.
*/ */
merge(other: Metadata): void { merge(other: Metadata): void {
forOwn(other.internalRepr, (values, key) => { other.internalRepr.forEach((values, key) => {
this.internalRepr[key] = (this.internalRepr[key] || []).concat(values); const mergedValue: MetadataValue[] =
(this.internalRepr.get(key) || []).concat(values);
this.internalRepr.set(key, mergedValue);
}); });
} }
@ -168,7 +171,7 @@ export class Metadata {
toHttp2Headers(): http2.OutgoingHttpHeaders { toHttp2Headers(): http2.OutgoingHttpHeaders {
// NOTE: Node <8.9 formats http2 headers incorrectly. // NOTE: Node <8.9 formats http2 headers incorrectly.
const result: http2.OutgoingHttpHeaders = {}; const result: http2.OutgoingHttpHeaders = {};
forOwn(this.internalRepr, (values, key) => { this.internalRepr.forEach((values, key) => {
// We assume that the user's interaction with this object is limited to // We assume that the user's interaction with this object is limited to
// through its public API (i.e. keys and values are already validated). // through its public API (i.e. keys and values are already validated).
result[key] = values.map((value) => { result[key] = values.map((value) => {

View File

@ -1,7 +1,7 @@
import * as assert from 'assert'; import * as assert from 'assert';
import * as http2 from 'http2'; import * as http2 from 'http2';
import {range} from 'lodash'; import {range} from 'lodash';
import {Metadata} from '../src/metadata'; import {Metadata, MetadataObject, MetadataValue} from '../src/metadata';
class TestMetadata extends Metadata { class TestMetadata extends Metadata {
getInternalRepresentation() { getInternalRepresentation() {
@ -277,21 +277,24 @@ describe('Metadata', () => {
}; };
const metadataFromHeaders = TestMetadata.fromHttp2Headers(headers); const metadataFromHeaders = TestMetadata.fromHttp2Headers(headers);
const internalRepr = metadataFromHeaders.getInternalRepresentation(); const internalRepr = metadataFromHeaders.getInternalRepresentation();
assert.deepEqual(internalRepr, { const expected: MetadataObject = new Map<string, MetadataValue[]>([
key1: ['value1'], ['key1', ['value1']], ['key2', ['value2']],
key2: ['value2'], ['key3', ['value3a', 'value3b']],
key3: ['value3a', 'value3b'], [
'key-bin': [ 'key-bin',
Buffer.from(range(0, 16)), Buffer.from(range(16, 32)), [
Buffer.from(range(0, 32)) Buffer.from(range(0, 16)), Buffer.from(range(16, 32)),
Buffer.from(range(0, 32))
]
] ]
}); ]);
assert.deepEqual(internalRepr, expected);
}); });
it('creates an empty Metadata object from empty headers', () => { it('creates an empty Metadata object from empty headers', () => {
const metadataFromHeaders = TestMetadata.fromHttp2Headers({}); const metadataFromHeaders = TestMetadata.fromHttp2Headers({});
const internalRepr = metadataFromHeaders.getInternalRepresentation(); const internalRepr = metadataFromHeaders.getInternalRepresentation();
assert.deepEqual(internalRepr, {}); assert.deepEqual(internalRepr, new Map<string, MetadataValue[]>());
}); });
}); });
}); });