Skip to content

Commit a2e72f3

Browse files
committed
ref: create direct pointers to tracing channels
1 parent 8362df0 commit a2e72f3

4 files changed

Lines changed: 132 additions & 129 deletions

File tree

src/__tests__/diagnostics-test.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,38 @@ import { describe, it } from 'mocha';
66

77
import { invariant } from '../jsutils/invariant.js';
88

9-
import { getChannels } from '../diagnostics.js';
9+
import {
10+
executeChannel,
11+
parseChannel,
12+
resolveChannel,
13+
subscribeChannel,
14+
validateChannel,
15+
} from '../diagnostics.js';
1016

1117
describe('diagnostics', () => {
1218
it('auto-registers the five graphql tracing channels', () => {
13-
const channels = getChannels();
14-
invariant(channels !== undefined);
19+
invariant(parseChannel !== undefined);
20+
invariant(validateChannel !== undefined);
21+
invariant(executeChannel !== undefined);
22+
invariant(subscribeChannel !== undefined);
23+
invariant(resolveChannel !== undefined);
1524

1625
// Node's `tracingChannel(name)` returns a fresh wrapper per call but
1726
// the underlying sub-channels are cached by name, so compare those.
18-
const byName = {
19-
execute: 'graphql:execute',
20-
parse: 'graphql:parse',
21-
validate: 'graphql:validate',
22-
resolve: 'graphql:resolve',
23-
subscribe: 'graphql:subscribe',
24-
} as const;
25-
for (const [key, name] of Object.entries(byName)) {
26-
expect(channels[key as keyof typeof byName].start).to.equal(
27-
dc.channel(`tracing:${name}:start`),
28-
);
29-
}
27+
expect(parseChannel.start).to.equal(
28+
dc.channel('tracing:graphql:parse:start'),
29+
);
30+
expect(validateChannel.start).to.equal(
31+
dc.channel('tracing:graphql:validate:start'),
32+
);
33+
expect(executeChannel.start).to.equal(
34+
dc.channel('tracing:graphql:execute:start'),
35+
);
36+
expect(subscribeChannel.start).to.equal(
37+
dc.channel('tracing:graphql:subscribe:start'),
38+
);
39+
expect(resolveChannel.start).to.equal(
40+
dc.channel('tracing:graphql:resolve:start'),
41+
);
3042
});
3143
});

src/diagnostics.ts

Lines changed: 34 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ function resolveDiagnosticsChannel(): DiagnosticsChannelModule | undefined {
8080
.getBuiltinModule === 'function'
8181
) {
8282
dc = (
83-
process as { getBuiltinModule: (id: string) => DiagnosticsChannelModule }
83+
process as {
84+
getBuiltinModule: (id: string) => DiagnosticsChannelModule;
85+
}
8486
).getBuiltinModule('node:diagnostics_channel');
8587
}
8688
if (!dc && typeof require === 'function') {
@@ -97,80 +99,56 @@ function resolveDiagnosticsChannel(): DiagnosticsChannelModule | undefined {
9799

98100
const dc = resolveDiagnosticsChannel();
99101

100-
const channels: GraphQLChannels | undefined = dc && {
101-
execute: dc.tracingChannel('graphql:execute'),
102-
parse: dc.tracingChannel('graphql:parse'),
103-
validate: dc.tracingChannel('graphql:validate'),
104-
resolve: dc.tracingChannel('graphql:resolve'),
105-
subscribe: dc.tracingChannel('graphql:subscribe'),
106-
};
107-
108102
/**
109-
* Internal accessor used at emission sites. Returns `undefined` when
110-
* `node:diagnostics_channel` isn't available on this runtime, allowing
111-
* emission sites to short-circuit on a single property access.
103+
* Per-channel handles, resolved once at module load. `undefined` when
104+
* `node:diagnostics_channel` isn't available. Emission sites read these
105+
* directly to keep the no-subscriber fast path to a single property access
106+
* plus a `hasSubscribers` check (no function calls, no closures).
112107
*
113108
* @internal
114109
*/
115-
export function getChannels(): GraphQLChannels | undefined {
116-
return channels;
117-
}
110+
export const parseChannel: MinimalTracingChannel | undefined =
111+
dc?.tracingChannel('graphql:parse');
112+
/** @internal */
113+
export const validateChannel: MinimalTracingChannel | undefined =
114+
dc?.tracingChannel('graphql:validate');
115+
/** @internal */
116+
export const executeChannel: MinimalTracingChannel | undefined =
117+
dc?.tracingChannel('graphql:execute');
118+
/** @internal */
119+
export const subscribeChannel: MinimalTracingChannel | undefined =
120+
dc?.tracingChannel('graphql:subscribe');
121+
/** @internal */
122+
export const resolveChannel: MinimalTracingChannel | undefined =
123+
dc?.tracingChannel('graphql:resolve');
118124

119125
/**
120-
* Gate for emission sites. Returns `true` when the named channel exists and
121-
* publishing should proceed.
122-
*
123-
* Uses `!== false` rather than a truthy check so runtimes which do not
124-
* implement the aggregated `hasSubscribers` getter on `TracingChannel` still
125-
* publish.
126+
* Publish a synchronous operation through `channel`. Caller has already
127+
* verified that a subscriber is attached; this helper exists only so the
128+
* traced path doesn't need to be duplicated at every emission site.
126129
*
127130
* @internal
128131
*/
129-
export function shouldTrace(
130-
channel: MinimalTracingChannel | undefined,
131-
): channel is MinimalTracingChannel {
132-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
133-
return channel !== undefined && channel.hasSubscribers !== false;
134-
}
135-
136-
/**
137-
* Publish a synchronous operation through the named graphql tracing channel,
138-
* short-circuiting to `fn()` when the channel isn't registered or nothing is
139-
* listening.
140-
*
141-
* @internal
142-
*/
143-
export function maybeTraceSync<T>(
144-
name: keyof GraphQLChannels,
145-
ctxFactory: () => object,
132+
export function traceSync<T>(
133+
channel: MinimalTracingChannel,
134+
ctx: object,
146135
fn: () => T,
147136
): T {
148-
const channel = getChannels()?.[name];
149-
if (!shouldTrace(channel)) {
150-
return fn();
151-
}
152-
return channel.traceSync(fn, ctxFactory());
137+
return channel.traceSync(fn, ctx);
153138
}
154139

155140
/**
156-
* Publish a mixed sync-or-promise operation through the named graphql tracing
157-
* channel.
141+
* Publish a mixed sync-or-promise operation through `channel`. Caller has
142+
* already verified that a subscriber is attached.
158143
*
159144
* @internal
160145
*/
161-
export function maybeTraceMixed<T>(
162-
name: keyof GraphQLChannels,
163-
ctxFactory: () => object,
146+
export function traceMixed<T>(
147+
channel: MinimalTracingChannel,
148+
ctxInput: object,
164149
fn: () => T | Promise<T>,
165150
): T | Promise<T> {
166-
const channel = getChannels()?.[name];
167-
if (!shouldTrace(channel)) {
168-
return fn();
169-
}
170-
const ctx = ctxFactory() as {
171-
error?: unknown;
172-
result?: unknown;
173-
};
151+
const ctx = ctxInput as { error?: unknown; result?: unknown };
174152

175153
return channel.start.runStores(ctx, () => {
176154
let result: T | Promise<T>;

src/language/parser.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Maybe } from '../jsutils/Maybe.js';
33
import type { GraphQLError } from '../error/GraphQLError.js';
44
import { syntaxError } from '../error/syntaxError.js';
55

6-
import { maybeTraceSync } from '../diagnostics.js';
6+
import { parseChannel } from '../diagnostics.js';
77

88
import type {
99
ArgumentCoordinateNode,
@@ -134,19 +134,23 @@ export function parse(
134134
source: string | Source,
135135
options?: ParseOptions,
136136
): DocumentNode {
137-
return maybeTraceSync(
138-
'parse',
139-
() => ({ source }),
140-
() => {
141-
const parser = new Parser(source, options);
142-
const document = parser.parseDocument();
143-
Object.defineProperty(document, 'tokenCount', {
144-
enumerable: false,
145-
value: parser.tokenCount,
146-
});
147-
return document;
148-
},
149-
);
137+
if (!parseChannel?.hasSubscribers) {
138+
return parseImpl(source, options);
139+
}
140+
return parseChannel.traceSync(() => parseImpl(source, options), { source });
141+
}
142+
143+
function parseImpl(
144+
source: string | Source,
145+
options: ParseOptions | undefined,
146+
): DocumentNode {
147+
const parser = new Parser(source, options);
148+
const document = parser.parseDocument();
149+
Object.defineProperty(document, 'tokenCount', {
150+
enumerable: false,
151+
value: parser.tokenCount,
152+
});
153+
return document;
150154
}
151155

152156
/**

src/validation/validate.ts

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { assertValidSchema } from '../type/validate.js';
1212

1313
import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo.js';
1414

15-
import { maybeTraceSync } from '../diagnostics.js';
15+
import { validateChannel } from '../diagnostics.js';
1616

1717
import { specifiedRules, specifiedSDLRules } from './specifiedRules.js';
1818
import type { SDLValidationRule, ValidationRule } from './ValidationContext.js';
@@ -63,52 +63,61 @@ export function validate(
6363
rules: ReadonlyArray<ValidationRule> = specifiedRules,
6464
options?: ValidationOptions,
6565
): ReadonlyArray<GraphQLError> {
66-
return maybeTraceSync(
67-
'validate',
68-
() => ({ schema, document: documentAST }),
69-
() => {
70-
const maxErrors = options?.maxErrors ?? 100;
71-
const hideSuggestions = options?.hideSuggestions ?? false;
72-
73-
// If the schema used for validation is invalid, throw an error.
74-
assertValidSchema(schema);
75-
76-
const errors: Array<GraphQLError> = [];
77-
const typeInfo = new TypeInfo(schema);
78-
const context = new ValidationContext(
79-
schema,
80-
documentAST,
81-
typeInfo,
82-
(error) => {
83-
if (errors.length >= maxErrors) {
84-
throw tooManyValidationErrorsError;
85-
}
86-
errors.push(error);
87-
},
88-
hideSuggestions,
89-
);
90-
91-
// This uses a specialized visitor which runs multiple visitors in
92-
// parallel, while maintaining the visitor skip and break API.
93-
const visitor = visitInParallel(rules.map((rule) => rule(context)));
94-
95-
// Visit the whole document with each instance of all provided rules.
96-
try {
97-
visit(
98-
documentAST,
99-
visitWithTypeInfo(typeInfo, visitor),
100-
QueryDocumentKeysToValidate,
101-
);
102-
} catch (e: unknown) {
103-
if (e === tooManyValidationErrorsError) {
104-
errors.push(tooManyValidationErrorsError);
105-
} else {
106-
throw e;
107-
}
66+
if (!validateChannel?.hasSubscribers) {
67+
return validateImpl(schema, documentAST, rules, options);
68+
}
69+
return validateChannel.traceSync(
70+
() => validateImpl(schema, documentAST, rules, options),
71+
{ schema, document: documentAST },
72+
);
73+
}
74+
75+
function validateImpl(
76+
schema: GraphQLSchema,
77+
documentAST: DocumentNode,
78+
rules: ReadonlyArray<ValidationRule>,
79+
options: ValidationOptions | undefined,
80+
): ReadonlyArray<GraphQLError> {
81+
const maxErrors = options?.maxErrors ?? 100;
82+
const hideSuggestions = options?.hideSuggestions ?? false;
83+
84+
// If the schema used for validation is invalid, throw an error.
85+
assertValidSchema(schema);
86+
87+
const errors: Array<GraphQLError> = [];
88+
const typeInfo = new TypeInfo(schema);
89+
const context = new ValidationContext(
90+
schema,
91+
documentAST,
92+
typeInfo,
93+
(error) => {
94+
if (errors.length >= maxErrors) {
95+
throw tooManyValidationErrorsError;
10896
}
109-
return errors;
97+
errors.push(error);
11098
},
99+
hideSuggestions,
111100
);
101+
102+
// This uses a specialized visitor which runs multiple visitors in
103+
// parallel, while maintaining the visitor skip and break API.
104+
const visitor = visitInParallel(rules.map((rule) => rule(context)));
105+
106+
// Visit the whole document with each instance of all provided rules.
107+
try {
108+
visit(
109+
documentAST,
110+
visitWithTypeInfo(typeInfo, visitor),
111+
QueryDocumentKeysToValidate,
112+
);
113+
} catch (e: unknown) {
114+
if (e === tooManyValidationErrorsError) {
115+
errors.push(tooManyValidationErrorsError);
116+
} else {
117+
throw e;
118+
}
119+
}
120+
return errors;
112121
}
113122

114123
/**

0 commit comments

Comments
 (0)