Skip to content

Commit b20a25e

Browse files
authored
fix(core/protocols): performance improvements for shape serde traversal (#7523)
1 parent 1592379 commit b20a25e

File tree

7 files changed

+203
-6
lines changed

7 files changed

+203
-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: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
const schema = [
9+
3,
10+
"ns",
11+
"Widget",
12+
0,
13+
["a", "b", "c", /*d,*/ "e", "f", "g", "h", "i", "j", "k", "l"],
14+
[0, [0, { jsonName: "B" }], [0, { idempotencyToken: 1 }], 0, 0, 0, 0, 0, 0, 0, 0],
15+
] satisfies StaticStructureSchema;
16+
17+
const ns = NormalizedSchema.of(schema);
18+
19+
describe("in serialization", () => {
20+
it("should iterate only the keys in the source object and any idempotency tokens", () => {
21+
expect(
22+
[
23+
...serializingStructIterator(ns, {
24+
d: "d",
25+
}),
26+
].map(([k]) => k)
27+
).toEqual([
28+
// a is ignored because it is not present and is not an idempotency token
29+
// b is ignored because it is not present and is not an idempotency token
30+
"c", // c is iterated because although it is not present, it is an idempotency token
31+
// d is ignored because although it is present, it is not part of the schema
32+
]);
33+
expect(
34+
[
35+
...serializingStructIterator(ns, {
36+
a: "a",
37+
b: "b",
38+
c: "c",
39+
d: "d",
40+
}),
41+
].map(([k]) => k)
42+
).toEqual(["a", "b", "c"]);
43+
});
44+
});
45+
46+
describe("in deserialization", () => {
47+
it("should only iterate the keys that exist on the source object, accounting for jsonName", () => {
48+
expect(
49+
[
50+
...deserializingStructIterator(
51+
ns,
52+
{
53+
B: "B",
54+
d: "d",
55+
},
56+
"jsonName"
57+
),
58+
].map(([k]) => k)
59+
).toEqual([
60+
// a is ignored because it is not present
61+
"b", // b is iterated because its jsonName counterpart is present
62+
// c is ignored because it is not present in the source object.
63+
// being an idempotencyToken doesn't mean anything in deserialization.
64+
// d is ignored because although it is present, it is not part of the schema
65+
]);
66+
67+
expect(
68+
[
69+
...deserializingStructIterator(
70+
ns,
71+
{
72+
a: "a",
73+
b: "b",
74+
c: "c",
75+
d: "d",
76+
},
77+
"jsonName"
78+
),
79+
].map(([k]) => k)
80+
).toEqual(["a", "c"]);
81+
expect(
82+
[
83+
...deserializingStructIterator(
84+
ns,
85+
{
86+
a: "a",
87+
b: "b",
88+
c: "c",
89+
d: "d",
90+
},
91+
false
92+
),
93+
].map(([k]) => k)
94+
).toEqual(["a", "b", "c"]);
95+
});
96+
97+
it("halts iteration once all keys from the source object have been iterated", () => {
98+
vi.spyOn(NormalizedSchema.prototype, "getMergedTraits");
99+
// regular iteration iterates all schema keys
100+
expect([...ns.structIterator()].map(([k]) => k)).toEqual(["a", "b", "c", "e", "f", "g", "h", "i", "j", "k", "l"]);
101+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(0);
102+
103+
vi.resetAllMocks();
104+
expect([...deserializingStructIterator(ns, { a: "a" }, "jsonName")].map(([k]) => k)).toEqual(["a"]);
105+
// only 1 call because iteration halts after 'a', since the total key count was 1.
106+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(1);
107+
108+
vi.resetAllMocks();
109+
expect([...deserializingStructIterator(ns, { a: "a", l: "l" }, "jsonName")].map(([k]) => k)).toEqual(["a", "l"]);
110+
// 11 calls because iteration continues in member order, and 'l' is the last key.
111+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(11);
112+
113+
vi.resetAllMocks();
114+
expect([...deserializingStructIterator(ns, { a: "a", l: "l" }, false)].map(([k]) => k)).toEqual(["a", "l"]);
115+
// no calls because no jsonName checking is involved.
116+
expect(NormalizedSchema.prototype.getMergedTraits).toHaveBeenCalledTimes(0);
117+
});
118+
});
119+
});
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)