Skip to content

Commit d4390fa

Browse files
committed
feat: add support for profiling through SANITY_DEBUG_PROFILING
1 parent e3e3ad1 commit d4390fa

File tree

7 files changed

+118
-42
lines changed

7 files changed

+118
-42
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,38 @@
1+
import {lstatSync} from 'node:fs'
2+
import {join} from 'node:path'
3+
14
import debugIt from 'debug'
25

36
export const debug = debugIt('sanity:core')
7+
8+
function isDir(path: string): boolean {
9+
try {
10+
return lstatSync(path).isDirectory()
11+
} catch {
12+
return false
13+
}
14+
}
15+
16+
/**
17+
* Runs a function such that it will be profiled when the environment variable
18+
* SANITY_DEBUG_PROFILING is set to a directory. A file (starting with `key`) will
19+
* be placed in said directory. The generated file can be inspected by using the
20+
* Speedscpe NPM package: `speedscope ${filename}` opens a UI in the browser.
21+
*/
22+
export async function withTracingProfiling<T>(key: string, fn: () => Promise<T>): Promise<T> {
23+
const dir = process.env.SANITY_DEBUG_PROFILING
24+
if (!dir) return await fn()
25+
26+
if (!isDir(dir))
27+
throw new Error(`SANITY_DEBUG_PROFILING (${JSON.stringify(dir)}) must be set to a directory`)
28+
29+
let profiling
30+
try {
31+
profiling = await import('./util/profiling')
32+
} catch (err) {
33+
throw new Error(`Failed to load SANITY_DEBUG_PROFILING: ${err}`)
34+
}
35+
36+
const filenamePrefix = join(dir, key)
37+
return profiling.withTracing(filenamePrefix, fn)
38+
}

packages/sanity/src/_internal/cli/threads/extractManifest.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_threads'
22

33
import {extractCreateWorkspaceManifest} from '../../manifest/extractWorkspaceManifest'
4+
import {withTracingProfiling} from '../debug'
45
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
56
import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment'
67

@@ -30,4 +31,4 @@ async function main() {
3031
}
3132
}
3233

33-
main()
34+
withTracingProfiling('extractManifest', main)

packages/sanity/src/_internal/cli/threads/extractSchema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {isMainThread, parentPort, workerData as _workerData} from 'node:worker_t
33
import {extractSchema} from '@sanity/schema/_internal'
44
import {type Workspace} from 'sanity'
55

6+
import {withTracingProfiling} from '../debug'
67
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
78
import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment'
89

@@ -48,7 +49,7 @@ async function main() {
4849
}
4950
}
5051

51-
main()
52+
withTracingProfiling('extractSchema', main)
5253

5354
function getWorkspace({
5455
workspaces,

packages/sanity/src/_internal/cli/threads/getGraphQLAPIs.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ import oneline from 'oneline'
77
import {type Workspace} from 'sanity'
88

99
import {type SchemaDefinitionish, type TypeResolvedGraphQLAPI} from '../actions/graphql/types'
10+
import {withTracingProfiling} from '../debug'
1011
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
1112

12-
if (isMainThread || !parentPort) {
13+
const port = parentPort
14+
15+
if (isMainThread || !port) {
1316
throw new Error('This module must be run as a worker thread')
1417
}
1518

16-
getGraphQLAPIsForked(parentPort)
19+
withTracingProfiling('getGraphQLAPIs', async () => getGraphQLAPIsForked(port))
1720

1821
async function getGraphQLAPIsForked(parent: MessagePort): Promise<void> {
1922
const {cliConfig, cliConfigPath, workDir} = workerData

packages/sanity/src/_internal/cli/threads/validateDocuments.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import {isReference, type ValidationContext, type ValidationMarker} from '@sanity/types'
1515
import {isRecord, validateDocument} from 'sanity'
1616

17+
import {withTracingProfiling} from '../debug'
1718
import {extractDocumentsFromNdjsonOrTarball} from '../util/extractDocumentsFromNdjsonOrTarball'
1819
import {getStudioWorkspaces} from '../util/getStudioWorkspaces'
1920
import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment'
@@ -129,7 +130,7 @@ async function* readerToGenerator(reader: ReadableStreamDefaultReader<Uint8Array
129130
}
130131
}
131132

132-
validateDocuments()
133+
withTracingProfiling('validateDocuments', validateDocuments)
133134

134135
async function loadWorkspace() {
135136
const workspaces = await getStudioWorkspaces({basePath: workDir, configPath})

packages/sanity/src/_internal/cli/threads/validateSchema.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {groupProblems, validateSchema} from '@sanity/schema/_internal'
44
import {type SchemaValidationProblem, type SchemaValidationProblemGroup} from '@sanity/types'
55
import {resolveSchemaTypes} from 'sanity'
66

7+
import {withTracingProfiling} from '../debug'
78
import {getStudioConfig} from '../util/getStudioWorkspaces'
89
import {mockBrowserEnvironment} from '../util/mockBrowserEnvironment'
910

@@ -29,49 +30,53 @@ if (isMainThread || !parentPort) {
2930
throw new Error('This module must be run as a worker thread')
3031
}
3132

32-
const cleanup = mockBrowserEnvironment(workDir)
33+
async function main() {
34+
const cleanup = mockBrowserEnvironment(workDir)
3335

34-
try {
35-
const workspaces = getStudioConfig({basePath: workDir})
36+
try {
37+
const workspaces = getStudioConfig({basePath: workDir})
3638

37-
if (!workspaces.length) {
38-
throw new Error(`Configuration did not return any workspaces.`)
39-
}
40-
41-
let workspace
42-
if (workspaceName) {
43-
workspace = workspaces.find((w) => w.name === workspaceName)
44-
if (!workspace) {
45-
throw new Error(`Could not find any workspaces with name \`${workspaceName}\``)
39+
if (!workspaces.length) {
40+
throw new Error(`Configuration did not return any workspaces.`)
4641
}
47-
} else {
48-
if (workspaces.length !== 1) {
49-
throw new Error(
50-
"Multiple workspaces found. Please specify which workspace to use with '--workspace'.",
51-
)
42+
43+
let workspace
44+
if (workspaceName) {
45+
workspace = workspaces.find((w) => w.name === workspaceName)
46+
if (!workspace) {
47+
throw new Error(`Could not find any workspaces with name \`${workspaceName}\``)
48+
}
49+
} else {
50+
if (workspaces.length !== 1) {
51+
throw new Error(
52+
"Multiple workspaces found. Please specify which workspace to use with '--workspace'.",
53+
)
54+
}
55+
workspace = workspaces[0]
5256
}
53-
workspace = workspaces[0]
54-
}
5557

56-
const schemaTypes = resolveSchemaTypes({
57-
config: workspace,
58-
context: {dataset: workspace.dataset, projectId: workspace.projectId},
59-
})
58+
const schemaTypes = resolveSchemaTypes({
59+
config: workspace,
60+
context: {dataset: workspace.dataset, projectId: workspace.projectId},
61+
})
6062

61-
const validation = groupProblems(validateSchema(schemaTypes).getTypes())
63+
const validation = groupProblems(validateSchema(schemaTypes).getTypes())
6264

63-
const result: ValidateSchemaWorkerResult = {
64-
validation: validation
65-
.map((group) => ({
66-
...group,
67-
problems: group.problems.filter((problem) =>
68-
level === 'error' ? problem.severity === 'error' : true,
69-
),
70-
}))
71-
.filter((group) => group.problems.length),
72-
}
65+
const result: ValidateSchemaWorkerResult = {
66+
validation: validation
67+
.map((group) => ({
68+
...group,
69+
problems: group.problems.filter((problem) =>
70+
level === 'error' ? problem.severity === 'error' : true,
71+
),
72+
}))
73+
.filter((group) => group.problems.length),
74+
}
7375

74-
parentPort?.postMessage(result)
75-
} finally {
76-
cleanup()
76+
parentPort?.postMessage(result)
77+
} finally {
78+
cleanup()
79+
}
7780
}
81+
82+
withTracingProfiling('validateSchema', main)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {writeFileSync} from 'node:fs'
2+
import {close as closeInspector, open as openInspector, Session} from 'node:inspector/promises'
3+
4+
// This file should not be imported directly since it depends on
5+
// `inspector/promises` which is only available since Node v19 (and we want to
6+
// support earlier Node versions as well.
7+
8+
/**
9+
* Runs a function with a tracing profiler and writes the result into a file.
10+
*
11+
* @param filenamePrefix - The filename where the report will be written. The full name
12+
* will be `{filenamePrefix}-{random}.cpuprofile`.
13+
*/
14+
export async function withTracing<T>(filenamePrefix: string, fn: () => Promise<T>): Promise<T> {
15+
// Make it available in the Chrome DevTools as well
16+
17+
openInspector()
18+
const session = new Session()
19+
session.connect()
20+
await session.post('Profiler.enable')
21+
await session.post('Profiler.start')
22+
try {
23+
return await fn()
24+
} finally {
25+
closeInspector()
26+
const fullname = `${filenamePrefix}-${Date.now()}-${Math.floor(Math.random() * 10000)}.cpuprofile`
27+
const {profile} = await session.post('Profiler.stop')
28+
writeFileSync(fullname, JSON.stringify(profile))
29+
}
30+
}

0 commit comments

Comments
 (0)