Skip to content

Commit 8c1c806

Browse files
committed
chore(core/protocols): performance improvements for shape serde traversal
1 parent 62f648d commit 8c1c806

File tree

7 files changed

+211
-6
lines changed

7 files changed

+211
-6
lines changed

packages/core/src/submodules/protocols/json/JsonShapeDeserializer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
import { fromBase64 } from "@smithy/util-base64";
1919

2020
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
21+
import { deserializingStructIterator } from "../structIterator";
2122
import { JsonSettings } from "./JsonCodec";
2223
import { jsonReviver } from "./jsonReviver";
2324
import { parseJsonBody } from "./parseJsonBody";
@@ -69,7 +70,11 @@ export class JsonShapeDeserializer extends SerdeContextConfig implements ShapeDe
6970
return out;
7071
} else if (ns.isStructSchema() && isObject) {
7172
const out = {} as any;
72-
for (const [memberName, memberSchema] of ns.structIterator()) {
73+
for (const [memberName, memberSchema] of deserializingStructIterator(
74+
ns,
75+
value,
76+
this.settings.jsonName ? "jsonName" : false
77+
)) {
7378
const fromKey = this.settings.jsonName ? memberSchema.getMergedTraits().jsonName ?? memberName : memberName;
7479
const deserializedValue = this._read(memberSchema, (value as any)[fromKey]);
7580
if (deserializedValue != null) {

packages/core/src/submodules/protocols/json/JsonShapeSerializer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
import { toBase64 } from "@smithy/util-base64";
1212

1313
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
14+
import { serializingStructIterator } from "../structIterator";
1415
import type { JsonSettings } from "./JsonCodec";
1516
import { JsonReplacer } from "./jsonReplacer";
1617

@@ -80,10 +81,10 @@ export class JsonShapeSerializer extends SerdeContextConfig implements ShapeSeri
8081
return out;
8182
} else if (ns.isStructSchema() && isObject) {
8283
const out = {} as any;
83-
for (const [memberName, memberSchema] of ns.structIterator()) {
84-
const targetKey = this.settings.jsonName ? memberSchema.getMergedTraits().jsonName ?? memberName : memberName;
84+
for (const [memberName, memberSchema] of serializingStructIterator(ns, value)) {
8585
const serializableValue = this._write(memberSchema, (value as any)[memberName], ns);
8686
if (serializableValue !== undefined) {
87+
const targetKey = this.settings.jsonName ? memberSchema.getMergedTraits().jsonName ?? memberName : memberName;
8788
out[targetKey] = serializableValue;
8889
}
8990
}

packages/core/src/submodules/protocols/json/experimental/SinglePassJsonShapeSerializer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
import { toBase64 } from "@smithy/util-base64";
1212

1313
import { SerdeContextConfig } from "../../ConfigurableSerdeContext";
14+
import { serializingStructIterator } from "../../structIterator";
1415
import type { JsonSettings } from "../JsonCodec";
1516

1617
/**
@@ -72,7 +73,7 @@ export class SinglePassJsonShapeSerializer extends SerdeContextConfig implements
7273
}
7374
} else if (ns.isStructSchema()) {
7475
b += "{";
75-
for (const [name, member] of ns.structIterator()) {
76+
for (const [name, member] of serializingStructIterator(ns, value)) {
7677
const item = (value as any)[name];
7778
const targetKey = this.settings.jsonName ? member.getMergedTraits().jsonName ?? name : name;
7879
const serializableValue = this.writeValue(member, item);

packages/core/src/submodules/protocols/query/QueryShapeSerializer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
import { toBase64 } from "@smithy/util-base64";
1313

1414
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
15+
import { serializingStructIterator } from "../structIterator";
1516
import type { QuerySerializerSettings } from "./QuerySerializerSettings";
1617

1718
/**
@@ -119,7 +120,7 @@ export class QueryShapeSerializer extends SerdeContextConfig implements ShapeSer
119120
}
120121
} else if (ns.isStructSchema()) {
121122
if (value && typeof value === "object") {
122-
for (const [memberName, member] of ns.structIterator()) {
123+
for (const [memberName, member] of serializingStructIterator(ns, value)) {
123124
if ((value as any)[memberName] == null && !member.isIdempotencyToken()) {
124125
continue;
125126
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { NormalizedSchema } from "@smithy/core/schema";
2+
import type { StaticStructureSchema } from "@smithy/types";
3+
import { describe, expect, test as it, vi } from "vitest";
4+
5+
import { deserializingStructIterator, serializingStructIterator } from "./structIterator";
6+
7+
describe("filtered struct iteration", () => {
8+
class ConstructorCountingNormalizedSchema extends (NormalizedSchema as any) {
9+
public static constructorCalls = 0;
10+
public constructor(...args: any[]) {
11+
ConstructorCountingNormalizedSchema.constructorCalls += 1;
12+
super(...args);
13+
}
14+
}
15+
16+
const schema = [
17+
3,
18+
"ns",
19+
"Widget",
20+
0,
21+
["a", "b", "c", /*d,*/ "e", "f", "g", "h", "i", "j", "k", "l"],
22+
[0, [0, { jsonName: "B" }], [0, { idempotencyToken: 1 }], 0, 0, 0, 0, 0, 0, 0, 0],
23+
] satisfies StaticStructureSchema;
24+
25+
const ns = ConstructorCountingNormalizedSchema.of(schema);
26+
27+
describe("in serialization", () => {
28+
it("should iterate only the keys in the source object and any idempotency tokens", () => {
29+
expect(
30+
[
31+
...serializingStructIterator(ns, {
32+
d: "d",
33+
}),
34+
].map(([k]) => k)
35+
).toEqual([
36+
// a is ignored because it is not present and is not an idempotency token
37+
// b is ignored because it is not present and is not an idempotency token
38+
"c", // c is iterated because although it is not present, it is an idempotency token
39+
// d is ignored because although it is present, it is not part of the schema
40+
]);
41+
expect(
42+
[
43+
...serializingStructIterator(ns, {
44+
a: "a",
45+
b: "b",
46+
c: "c",
47+
d: "d",
48+
}),
49+
].map(([k]) => k)
50+
).toEqual(["a", "b", "c"]);
51+
});
52+
});
53+
54+
describe("in deserialization", () => {
55+
it("should only iterate the keys that exist on the source object, accounting for jsonName", () => {
56+
expect(
57+
[
58+
...deserializingStructIterator(
59+
ns,
60+
{
61+
B: "B",
62+
d: "d",
63+
},
64+
"jsonName"
65+
),
66+
].map(([k]) => k)
67+
).toEqual([
68+
// a is ignored because it is not present
69+
"b", // b is iterated because its jsonName counterpart is present
70+
// c is ignored because it is not present in the source object.
71+
// being an idempotencyToken doesn't mean anything in deserialization.
72+
// d is ignored because although it is present, it is not part of the schema
73+
]);
74+
75+
expect(
76+
[
77+
...deserializingStructIterator(
78+
ns,
79+
{
80+
a: "a",
81+
b: "b",
82+
c: "c",
83+
d: "d",
84+
},
85+
"jsonName"
86+
),
87+
].map(([k]) => k)
88+
).toEqual(["a", "c"]);
89+
expect(
90+
[
91+
...deserializingStructIterator(
92+
ns,
93+
{
94+
a: "a",
95+
b: "b",
96+
c: "c",
97+
d: "d",
98+
},
99+
false
100+
),
101+
].map(([k]) => k)
102+
).toEqual(["a", "b", "c"]);
103+
});
104+
105+
it("halts iteration once all keys from the source object have been iterated", () => {
106+
vi.spyOn(NormalizedSchema.prototype, "getMergedTraits");
107+
// regular iteration iterates all schema keys
108+
expect([...ns.structIterator()].map(([k]) => k)).toEqual(["a", "b", "c", "e", "f", "g", "h", "i", "j", "k", "l"]);
109+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(0);
110+
111+
vi.resetAllMocks();
112+
expect([...deserializingStructIterator(ns, { a: "a" }, "jsonName")].map(([k]) => k)).toEqual(["a"]);
113+
// only 1 call because iteration halts after 'a', since the total key count was 1.
114+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(1);
115+
116+
vi.resetAllMocks();
117+
expect([...deserializingStructIterator(ns, { a: "a", l: "l" }, "jsonName")].map(([k]) => k)).toEqual(["a", "l"]);
118+
// 11 calls because iteration continues in member order, and 'l' is the last key.
119+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(11);
120+
121+
vi.resetAllMocks();
122+
expect([...deserializingStructIterator(ns, { a: "a", l: "l" }, false)].map(([k]) => k)).toEqual(["a", "l"]);
123+
// no calls because no jsonName checking is involved.
124+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(0);
125+
});
126+
});
127+
});
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { NormalizedSchema } from "@smithy/core/schema";
2+
import type { StaticStructureSchema } from "@smithy/types";
3+
4+
/**
5+
* @internal
6+
*/
7+
type SourceObject = Record<string, any>;
8+
9+
/**
10+
* For serialization use only.
11+
* @internal
12+
*
13+
* @param ns - normalized schema object.
14+
* @param sourceObject - source object from serialization.
15+
*/
16+
export function* serializingStructIterator(ns: NormalizedSchema, sourceObject: SourceObject) {
17+
if (ns.isUnitSchema()) {
18+
return;
19+
}
20+
const struct = ns.getSchema() as StaticStructureSchema;
21+
for (let i = 0; i < struct[4].length; ++i) {
22+
const key = struct[4][i];
23+
const memberNs = new (NormalizedSchema as any)([struct[5][i], 0], key);
24+
if (!(key in sourceObject) && !memberNs.isIdempotencyToken()) {
25+
continue;
26+
}
27+
yield [key, memberNs];
28+
}
29+
}
30+
31+
/**
32+
* For deserialization use only.
33+
* Yields a subset of NormalizedSchema::structIterator matched to the source object keys.
34+
* This is a performance optimization to avoid creation of NormalizedSchema member
35+
* objects for members that are undefined in the source data object but may be numerous
36+
* in the schema/model.
37+
* @internal
38+
*
39+
* @param ns - normalized schema object.
40+
* @param sourceObject - source object from deserialization.
41+
* @param nameTrait - xmlName or jsonName trait to look for.
42+
*/
43+
export function* deserializingStructIterator(
44+
ns: NormalizedSchema,
45+
sourceObject: SourceObject,
46+
nameTrait?: "xmlName" | "jsonName" | false
47+
) {
48+
if (ns.isUnitSchema()) {
49+
return;
50+
}
51+
const struct = ns.getSchema() as StaticStructureSchema;
52+
let keysRemaining = Object.keys(sourceObject).length;
53+
for (let i = 0; i < struct[4].length; ++i) {
54+
if (keysRemaining === 0) {
55+
break;
56+
}
57+
const key = struct[4][i];
58+
const memberNs = new (NormalizedSchema as any)([struct[5][i], 0], key);
59+
let serializationKey = key;
60+
if (nameTrait) {
61+
serializationKey = memberNs.getMergedTraits()[nameTrait] ?? key;
62+
}
63+
if (!(serializationKey in sourceObject)) {
64+
continue;
65+
}
66+
yield [key, memberNs];
67+
keysRemaining -= 1;
68+
}
69+
}

packages/core/src/submodules/protocols/xml/XmlShapeSerializer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
import { fromBase64, toBase64 } from "@smithy/util-base64";
1414

1515
import { SerdeContextConfig } from "../ConfigurableSerdeContext";
16+
import { serializingStructIterator } from "../structIterator";
1617
import { XmlSettings } from "./XmlCodec";
1718

1819
type XmlNamespaceAttributeValuePair = [string, string] | [undefined, undefined];
@@ -86,7 +87,7 @@ export class XmlShapeSerializer extends SerdeContextConfig implements ShapeSeria
8687

8788
const [xmlnsAttr, xmlns] = this.getXmlnsAttribute(ns, parentXmlns);
8889

89-
for (const [memberName, memberSchema] of ns.structIterator()) {
90+
for (const [memberName, memberSchema] of serializingStructIterator(ns, value as any)) {
9091
const val = (value as any)[memberName];
9192

9293
if (val != null || memberSchema.isIdempotencyToken()) {

0 commit comments

Comments
 (0)