Add tracing to IDENTIFY

This commit is contained in:
Rory& 2025-07-09 16:41:37 +02:00
parent 654e07bbf5
commit 230248d6f4
5 changed files with 129 additions and 46 deletions

View File

@ -56,6 +56,11 @@ import {
checkToken, checkToken,
emitEvent, emitEvent,
getDatabase, getDatabase,
TraceNode,
TraceRoot,
Stopwatch,
timePromise,
ElapsedTime,
} from "@spacebar/util"; } from "@spacebar/util";
import { check } from "./instanceOf"; import { check } from "./instanceOf";
@ -71,7 +76,10 @@ const tryGetUserFromToken = async (...args: Parameters<typeof checkToken>) => {
}; };
export async function onIdentify(this: WebSocket, data: Payload) { export async function onIdentify(this: WebSocket, data: Payload) {
const startTime = Date.now(); const totalSw = Stopwatch.startNew();
const taskSw = Stopwatch.startNew();
const gatewayShardName = `sb-gateway`;
if (this.user_id) { if (this.user_id) {
// we've already identified // we've already identified
return this.close(CLOSECODES.Already_authenticated); return this.close(CLOSECODES.Already_authenticated);
@ -85,6 +93,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
this.capabilities = new Capabilities(identify.capabilities || 0); this.capabilities = new Capabilities(identify.capabilities || 0);
this.large_threshold = identify.large_threshold || 250; this.large_threshold = identify.large_threshold || 250;
const parseAndValidateTime = taskSw.getElapsedAndReset();
const user = await tryGetUserFromToken(identify.token, { const user = await tryGetUserFromToken(identify.token, {
relations: ["relationships", "relationships.to", "settings"], relations: ["relationships", "relationships.to", "settings"],
@ -92,10 +101,11 @@ export async function onIdentify(this: WebSocket, data: Payload) {
}); });
if (!user) return this.close(CLOSECODES.Authentication_failed); if (!user) return this.close(CLOSECODES.Authentication_failed);
this.user_id = user.id; this.user_id = user.id;
const userQueryTime = Date.now(); const userQueryTime = taskSw.getElapsedAndReset();
// Check intents // Check intents
if (!identify.intents) identify.intents = 0b11011111111111111111111111111111111n; // TODO: what is this number? if (!identify.intents)
identify.intents = 0b11011111111111111111111111111111111n; // TODO: what is this number?
this.intents = new Intents(identify.intents); this.intents = new Intents(identify.intents);
// TODO: actually do intent things. // TODO: actually do intent things.
@ -119,6 +129,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
return this.close(CLOSECODES.Invalid_shard); return this.close(CLOSECODES.Invalid_shard);
} }
} }
const validateIntentsAndShardingTime = taskSw.getElapsedAndReset();
// Generate a new gateway session ( id is already made, just save it in db ) // Generate a new gateway session ( id is already made, just save it in db )
const session = Session.create({ const session = Session.create({
@ -133,23 +144,30 @@ export async function onIdentify(this: WebSocket, data: Payload) {
client_status: {}, client_status: {},
activities: identify.presence?.activities, // TODO: validation activities: identify.presence?.activities, // TODO: validation
}); });
const createSessionTime = taskSw.getElapsedAndReset();
const createSessionTime = Date.now();
// Get from database: // Get from database:
// * the users read states // * the users read states
// * guild members for this user // * guild members for this user
// * recipients ( dm channels ) // * recipients ( dm channels )
// * the bot application, if it exists // * the bot application, if it exists
const [, application, read_states, members, recipients] = await Promise.all( const [
[ { elapsed: sessionSaveTime },
session.save(), { result: application, elapsed: applicationQueryTime },
{ result: read_states, elapsed: read_statesQueryTime },
{ result: members, elapsed: membersQueryTime },
{ result: recipients, elapsed: recipientsQueryTime },
] = await Promise.all([
timePromise(() => session.save()),
timePromise(() =>
Application.findOne({ Application.findOne({
where: { id: this.user_id }, where: { id: this.user_id },
select: ["id", "flags"], select: ["id", "flags"],
}), }),
),
timePromise(() =>
ReadState.find({ ReadState.find({
where: { user_id: this.user_id }, where: { user_id: this.user_id },
select: [ select: [
@ -160,7 +178,9 @@ export async function onIdentify(this: WebSocket, data: Payload) {
"mention_count", "mention_count",
], ],
}), }),
),
timePromise(() =>
Member.find({ Member.find({
where: { id: this.user_id }, where: { id: this.user_id },
select: { select: {
@ -194,7 +214,9 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// "user", // "user",
], ],
}), }),
),
timePromise(() =>
Recipient.find({ Recipient.find({
where: { user_id: this.user_id, closed: false }, where: { user_id: this.user_id, closed: false },
relations: [ relations: [
@ -226,17 +248,19 @@ export async function onIdentify(this: WebSocket, data: Payload) {
}, },
}, },
}), }),
], ),
); ]);
const query1Time = Date.now(); const totalQueryTime = taskSw.getElapsedAndReset();
// We forgot to migrate user settings from the JSON column of `users` // We forgot to migrate user settings from the JSON column of `users`
// to the `user_settings` table theyre in now, // to the `user_settings` table theyre in now,
// so for instances that migrated, users may not have a `user_settings` row. // so for instances that migrated, users may not have a `user_settings` row.
let createUserSettingsTime: ElapsedTime | undefined = undefined;
if (!user.settings) { if (!user.settings) {
user.settings = new UserSettings(); user.settings = new UserSettings();
await user.settings.save(); await user.settings.save();
createUserSettingsTime = taskSw.getElapsedAndReset();
} }
// Generate merged_members // Generate merged_members
@ -262,8 +286,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
}, },
]; ];
}); });
const mergedMembersTime = taskSw.getElapsedAndReset();
const mergedMembersTime = Date.now();
// Populated with guilds 'unavailable' currently // Populated with guilds 'unavailable' currently
// Just for bots // Just for bots
@ -307,6 +330,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
threads: [], threads: [],
}; };
}); });
const generateGuildsListTime = taskSw.getElapsedAndReset();
// Generate user_guild_settings // Generate user_guild_settings
const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] = const user_guild_settings_entries: ReadyUserGuildSettingsEntries[] =
@ -321,6 +345,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
channel_id: y[0], channel_id: y[0],
})), })),
})); }));
const generateUserGuildSettingsTime = taskSw.getElapsedAndReset();
// Popultaed with users from private channels, relationships. // Popultaed with users from private channels, relationships.
// Uses a set to dedupe for us. // Uses a set to dedupe for us.
@ -355,11 +380,11 @@ export async function onIdentify(this: WebSocket, data: Payload) {
is_spam: false, // TODO is_spam: false, // TODO
}; };
}); });
const generateDmChannelsTime = taskSw.getElapsedAndReset();
// From user relationships ( friends ), also append to `users` list // From user relationships ( friends ), also append to `users` list
user.relationships.forEach((x) => users.add(x.to.toPublicUser())); user.relationships.forEach((x) => users.add(x.to.toPublicUser()));
const appendRelationshipsTime = taskSw.getElapsedAndReset();
const remapDataTime = Date.now();
// Send SESSIONS_REPLACE and PRESENCE_UPDATE // Send SESSIONS_REPLACE and PRESENCE_UPDATE
const allSessions = ( const allSessions = (
@ -376,25 +401,31 @@ export async function onIdentify(this: WebSocket, data: Payload) {
session_id: x.session_id, // TODO: discord.com sends 'all', what is that??? session_id: x.session_id, // TODO: discord.com sends 'all', what is that???
status: x.status, status: x.status,
})); }));
const findAndGenerateSessionReplaceTime = taskSw.getElapsedAndReset();
const sessionReplaceTime = Date.now(); const [
{ elapsed: emitSessionsReplaceTime },
Promise.all([ { elapsed: emitPresenceUpdateTime },
emitEvent({ ] = await Promise.all([
event: "SESSIONS_REPLACE", timePromise(() =>
user_id: this.user_id, emitEvent({
data: allSessions, event: "SESSIONS_REPLACE",
} as SessionsReplace), user_id: this.user_id,
emitEvent({ data: allSessions,
event: "PRESENCE_UPDATE", } as SessionsReplace),
user_id: this.user_id, ),
data: { timePromise(() =>
user: user.toPublicUser(), emitEvent({
activities: session.activities, event: "PRESENCE_UPDATE",
client_status: session.client_status, user_id: this.user_id,
status: session.status, data: {
}, user: user.toPublicUser(),
} as PresenceUpdateEvent), activities: session.activities,
client_status: session.client_status,
status: session.status,
},
} as PresenceUpdateEvent),
),
]); ]);
// Build READY // Build READY
@ -402,6 +433,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
read_states.forEach((x) => { read_states.forEach((x) => {
x.id = x.channel_id; x.id = x.channel_id;
}); });
const remapReadStateIdsTime = taskSw.getElapsedAndReset();
const d: ReadyEventData = { const d: ReadyEventData = {
v: 9, v: 9,
@ -462,7 +494,49 @@ export async function onIdentify(this: WebSocket, data: Payload) {
// ???? // ????
flags: 0, flags: 0,
}, },
game_relationships: [],
}; };
const buildReadyEventDataTime = taskSw.getElapsedAndReset();
const _trace = [
gatewayShardName,
{
micros: totalSw.elapsed().totalMicroseconds,
calls: [],
},
] as TraceRoot;
const times = {
parseAndValidateTime,
userQueryTime,
validateIntentsAndShardingTime,
createSessionTime,
sessionSaveTime,
applicationQueryTime,
read_statesQueryTime,
membersQueryTime,
recipientsQueryTime,
totalQueryTime,
createUserSettingsTime,
mergedMembersTime,
generateGuildsListTime,
generateUserGuildSettingsTime,
generateDmChannelsTime,
appendRelationshipsTime,
findAndGenerateSessionReplaceTime,
emitSessionsReplaceTime,
emitPresenceUpdateTime,
remapReadStateIdsTime,
buildReadyEventDataTime,
};
for (const [key, value] of Object.entries(times)) {
if (value) {
_trace![1].calls.push(key, { micros: value.totalMicroseconds });
}
}
_trace![1].calls.push(
"buildTraceTime",
{ micros: taskSw.elapsed().totalMicroseconds },
);
d._trace = [JSON.stringify(_trace)];
// Send READY // Send READY
await Send(this, { await Send(this, {
@ -547,16 +621,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const setupListenerTime = Date.now(); const setupListenerTime = Date.now();
console.log( console.log(
`[Gateway] IDENTIFY ${this.user_id} in ${Date.now() - startTime}ms`, `[Gateway] IDENTIFY ${this.user_id} in ${totalSw.elapsed().totalMilliseconds}ms`,
{ JSON.stringify(d._trace, null, 2),
userQueryTime: Date.now() - userQueryTime,
createSessionTime: Date.now() - createSessionTime,
query1Time: Date.now() - query1Time,
remapDataTime: Date.now() - remapDataTime,
sessionReplaceTime: Date.now() - sessionReplaceTime,
readyTime: Date.now() - readyTime,
pendingGuildsTime: Date.now() - pendingGuildsTime,
readySupplementalTime: Date.now() - readySupplementalTime,
}
); );
} }

View File

@ -41,6 +41,11 @@ export enum OPCODES {
Stream_Ping = 21, Stream_Ping = 21,
Stream_Set_Paused = 22, Stream_Set_Paused = 22,
Request_Application_Commands = 24, Request_Application_Commands = 24,
// We don't know the names for these:
ThirtySix = 36, // this is just a guild id?
Guild_Subscriptions_Bulk = 37, // Already implemented it seems?
SetQoS = 40,
ClientInitSession = 41,
} }
export enum CLOSECODES { export enum CLOSECODES {
Unknown_error = 4000, Unknown_error = 4000,

View File

@ -182,7 +182,7 @@ export class ReadyGuildDTO implements IReadyGuildDTO {
1: 5, 1: 5,
2: 2, 2: 2,
3: 2, 3: 2,
}; // ????? }; // ????? // emma: this appears to always be an empty attrset...
this.channels = guild.channels; this.channels = guild.channels;
this.data_mode = "full"; this.data_mode = "full";
this.emojis = guild.emojis; this.emojis = guild.emojis;

View File

@ -132,8 +132,17 @@ export interface ReadyEventData {
notification_settings: { notification_settings: {
flags: number; flags: number;
}; };
game_relationships: never[]; // what is this?
_trace?: string[]; // trace of the request, used for debugging
} }
export type TraceNode =
| { micros: number; calls: TraceNode[] }
| { micros: number }
| string;
export type TraceRoot = [string, { micros: number; calls: TraceNode[] }];
export interface ReadyEvent extends Event { export interface ReadyEvent extends Event {
event: "READY"; event: "READY";
data: ReadyEventData; data: ReadyEventData;
@ -159,7 +168,10 @@ export interface ChannelPinsUpdateEvent extends Event {
data: { data: {
guild_id?: string; guild_id?: string;
channel_id: string; channel_id: string;
last_pin_timestamp?: number; /**
* @format ISO8601
*/
last_pin_timestamp?: string;
}; };
} }

View File

@ -39,6 +39,7 @@ export * from "./Regex";
export * from "./Rights"; export * from "./Rights";
export * from "./Sentry"; export * from "./Sentry";
export * from "./Snowflake"; export * from "./Snowflake";
export * from "./Stopwatch";
export * from "./String"; export * from "./String";
export * from "./Token"; export * from "./Token";
export * from "./TraverseDirectory"; export * from "./TraverseDirectory";