Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve test runner performance with turbo #48308

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3179914
Document how to visualize traces
jankaifer Apr 12, 2023
fc51e01
add traces into package packing
jankaifer Apr 12, 2023
675f64f
remove debug log
jankaifer Apr 12, 2023
72ec23a
don't copy .tgz files in our tests
jankaifer Apr 12, 2023
157434a
don't cleanup temporary packing directories when using skip_cleanup flag
jankaifer Apr 12, 2023
549c959
added thread safe test-pack
jankaifer Apr 12, 2023
988b3fe
Remove unused code and added fallback for comparison test with latest…
jankaifer Apr 12, 2023
1f5245d
fix test-pack cache-poison with stale dist/
jankaifer Apr 12, 2023
74a8508
don't forward test-pack stdout when running tests
jankaifer Apr 12, 2023
e453022
run test-pack concurrently
jankaifer Apr 12, 2023
263477e
remove useless dependency from turbo
jankaifer Apr 12, 2023
13cae87
remove unused code
jankaifer Apr 12, 2023
84a9105
improved description of non-concurrent.mts
jankaifer Apr 12, 2023
84ef94b
Merge branch 'canary' into jankaifer/next-1002-investigate-caching-of…
jankaifer Apr 12, 2023
db2766b
Merge remote-tracking branch 'origin/canary' into jankaifer/next-1002…
jankaifer Apr 13, 2023
135b02f
Revert "Only create tarballs once per run-tests (#48321)"
jankaifer Apr 13, 2023
aaef63e
remove unused directory
jankaifer Apr 13, 2023
f2a9b62
readd reverted part of code that was useful
jankaifer Apr 13, 2023
b22bfd5
Hide test pack behind feature flag
jankaifer Apr 13, 2023
2b0dd2c
fix feature flag not being there
jankaifer Apr 13, 2023
7dc4672
Only create tarballs once per run-tests (#48321)
ijjk Apr 13, 2023
b5f60eb
fix flag usage where we would link twice
jankaifer Apr 13, 2023
1383a0d
Remove fallback for stats ci workflow - we don't need it with feature…
jankaifer Apr 13, 2023
81668a5
Merge branch 'canary' into jankaifer/next-1002-investigate-caching-of…
jankaifer Apr 13, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions .github/actions/next-stats-action/src/prepare/repo-setup.js
Expand Up @@ -55,14 +55,13 @@ module.exports = (actionInfo) => {
}
},
async linkPackages({ repoDir, nextSwcVersion }) {
let useTestPack = process.env.NEXT_TEST_PACK
const useTurbo = Boolean(process.env.NEXT_TEST_PACK)

if (useTestPack) {
execa.sync('pnpm', ['turbo', 'run', 'test-pack'], {
if (useTurbo) {
execa.sync('pnpm', ['test-pack-all'], {
cwd: repoDir,
env: { NEXT_SWC_VERSION: nextSwcVersion },
})

const pkgPaths = new Map()
const pkgs = (await fs.readdir(path.join(repoDir, 'packages'))).filter(
(item) => !item.startsWith('.')
Expand All @@ -87,7 +86,6 @@ module.exports = (actionInfo) => {
})
return pkgPaths
} else {
// TODO: remove after next stable release (current v13.1.2)
const pkgPaths = new Map()
const pkgDatas = new Map()
let pkgs
Expand Down
1 change: 1 addition & 0 deletions contributing/core/testing.md
Expand Up @@ -79,3 +79,4 @@ When tests are run in CI and a test failure occurs we attempt to capture traces
### Profiling tests

Add `NEXT_TEST_TRACE=1` to enable test profiling. It's useful for improving our testing infrastructure.
Those traces can be visualized with `node scripts/trace-to-tree.mjs test/.trace/trace`.
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -23,7 +23,9 @@
"test": "pnpm testheadless",
"testonly": "jest --runInBand",
"testheadless": "cross-env HEADLESS=true pnpm testonly",
"test-pack": "cross-env TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm scripts/test-pack-package.mts",
"test-pack-all": "pnpm exec-ts scripts/non-concurrent.mts packing-turbo pnpm turbo run test-pack --concurrency=100%",
"test-pack": "pnpm exec-ts scripts/test-pack-package.mts",
"exec-ts": "cross-env TS_NODE_TRANSPILE_ONLY=1 node --loader ts-node/esm",
"genstats": "cross-env LOCAL_STATS=true node .github/actions/next-stats-action/src/index.js",
"git-reset": "git reset --hard HEAD",
"git-clean": "git clean -d -x -e node_modules -e packages -f",
Expand Down
6 changes: 1 addition & 5 deletions run-tests.js
Expand Up @@ -9,6 +9,7 @@ const { promisify } = require('util')
const { Sema } = require('async-sema')
const { spawn, exec: execOrig } = require('child_process')
const { createNextInstall } = require('./test/lib/create-next-install')
const { mockTrace } = require('./test/lib/mock-trace')
const glob = promisify(_glob)
const exec = promisify(execOrig)

Expand Down Expand Up @@ -43,11 +44,6 @@ const testFilters = {
examples: 'examples/',
}

const mockTrace = () => ({
traceAsyncFn: (fn) => fn(mockTrace()),
traceChild: () => mockTrace(),
})

// which types we have configured to run separate
const configuredTestTypes = Object.values(testFilters)

Expand Down
71 changes: 71 additions & 0 deletions scripts/non-concurrent.mts
@@ -0,0 +1,71 @@
import path from 'path'
import execa from 'execa'
import fs from 'fs-extra'
import { fileURLToPath } from 'url'

/**
* Make sure that script passed a arguments is not run concurrently.
* It will wait for other invocations with the same `operation-id` to finish before running.
*
* Usage:
* node scripts/non-concurrent.mts [operation-id] [script with arguments]...
* Example:
* node scripts/non-concurrent.mts test-pack node scripts/test-pack.mts --more --args
*/

const timeoutMs = 100
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

const cleanupFns: (() => void)[] = []

const main = async () => {
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const repoRoot = path.dirname(__dirname)
const operationId = process.argv[2]
const nonConcurrentFolder = path.join(
repoRoot,
'test',
'tmp',
'nonConcurrent'
)
const lockFolder = path.join(nonConcurrentFolder, `${operationId}.lock`)

while (true) {
// Create a file but throw if it already exists, use fs module
try {
await fs.ensureDir(nonConcurrentFolder)
await fs.mkdir(lockFolder)
cleanupFns.push(() => fs.rmdirSync(lockFolder, { recursive: true }))
} catch (err) {
if (err.code === 'EEXIST') {
console.log(`Waiting for other invocations to finish...`)
await sleep(timeoutMs)
continue
}
throw err
}

const proc = execa(process.argv[3], process.argv.slice(4))
proc.stdout?.pipe(process.stdout)
proc.stderr?.pipe(process.stderr)
await proc
return
}
}

const exitHandler = async () => {
for (const fn of cleanupFns) {
try {
fn()
} catch (err) {}
}
process.exit()
}

process.on('exit', exitHandler)
process.on('SIGINT', exitHandler)
process.on('SIGUSR1', exitHandler)
process.on('SIGUSR2', exitHandler)
process.on('uncaughtException', exitHandler)

main()
1 change: 1 addition & 0 deletions scripts/test-pack-package.mts
Expand Up @@ -40,6 +40,7 @@ const main = async () => {
// There's a bug in `pnpm pack` where it will run
// the prepublishOnly script and that will fail.
// See https://github.com/pnpm/pnpm/issues/2941
// This is fixed in v7.26.0
delete packageJson.scripts.prepublishOnly
}

Expand Down
158 changes: 89 additions & 69 deletions test/lib/create-next-install.js
Expand Up @@ -21,6 +21,7 @@ async function createNextInstall({
.traceAsyncFn(async (rootSpan) => {
const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir())
const origRepoDir = path.join(__dirname, '../../')
const useTurbo = Boolean(process.env.NEXT_TEST_PACK)
const installDir = path.join(
tmpDir,
`next-install-${randomBytes(32).toString('hex')}${dirSuffix}`
Expand All @@ -30,85 +31,103 @@ async function createNextInstall({
require('console').log(installDir)

let pkgPaths = process.env.NEXT_TEST_PKG_PATHS
if (!useTurbo) {
if (pkgPaths) {
pkgPaths = new Map(JSON.parse(pkgPaths))
require('console').log('using provided pkg paths')
} else {
tmpRepoDir = path.join(
tmpDir,
`next-repo-${randomBytes(32).toString('hex')}${dirSuffix}`
)
require('console').log('Creating temp repo dir', tmpRepoDir)

if (pkgPaths) {
pkgPaths = new Map(JSON.parse(pkgPaths))
require('console').log('using provided pkg paths')
} else {
tmpRepoDir = path.join(
tmpDir,
`next-repo-${randomBytes(32).toString('hex')}${dirSuffix}`
)
require('console').log('Creating temp repo dir', tmpRepoDir)

await rootSpan
.traceChild('ensure swc binary')
.traceAsyncFn(async () => {
// ensure swc binary is present in the native folder if
// not already built
for (const folder of await fs.readdir(
path.join(origRepoDir, 'node_modules/@next')
)) {
if (folder.startsWith('swc-')) {
const swcPkgPath = path.join(
origRepoDir,
'node_modules/@next',
folder
)
const outputPath = path.join(
origRepoDir,
'packages/next-swc/native'
)
await fs.copy(swcPkgPath, outputPath, {
filter: (item) => {
return (
item === swcPkgPath ||
(item.endsWith('.node') &&
!fs.pathExistsSync(
path.join(outputPath, path.basename(item))
))
)
},
})
}
}
})

for (const item of ['package.json', 'packages']) {
await rootSpan
.traceChild(`copy ${item} to temp dir`)
.traceChild('ensure swc binary')
.traceAsyncFn(async () => {
await fs.copy(
path.join(origRepoDir, item),
path.join(tmpRepoDir, item),
{
filter: (item) => {
return (
!item.includes('node_modules') &&
!item.includes('pnpm-lock.yaml') &&
!item.includes('.DS_Store') &&
// Exclude Rust compilation files
!/next[\\/]build[\\/]swc[\\/]target/.test(item) &&
!/next-swc[\\/]target/.test(item)
)
},
// ensure swc binary is present in the native folder if
// not already built
for (const folder of await fs.readdir(
path.join(origRepoDir, 'node_modules/@next')
)) {
if (folder.startsWith('swc-')) {
const swcPkgPath = path.join(
origRepoDir,
'node_modules/@next',
folder
)
const outputPath = path.join(
origRepoDir,
'packages/next-swc/native'
)
await fs.copy(swcPkgPath, outputPath, {
filter: (item) => {
return (
item === swcPkgPath ||
(item.endsWith('.node') &&
!fs.pathExistsSync(
path.join(outputPath, path.basename(item))
))
)
},
})
}
)
}
})
}

pkgPaths = await rootSpan.traceChild('linkPackages').traceAsyncFn(() =>
linkPackages({
repoDir: tmpRepoDir,
})
)
for (const item of ['package.json', 'packages']) {
await rootSpan
.traceChild(`copy ${item} to temp dir`)
.traceAsyncFn(async () => {
await fs.copy(
path.join(origRepoDir, item),
path.join(tmpRepoDir, item),
{
filter: (item) => {
return (
!item.includes('node_modules') &&
!item.includes('pnpm-lock.yaml') &&
!item.includes('.DS_Store') &&
// Exclude Rust compilation files
!/next[\\/]build[\\/]swc[\\/]target/.test(item) &&
!/next-swc[\\/]target/.test(item)
)
},
}
)
})
}

pkgPaths = await rootSpan
.traceChild('linkPackages')
.traceAsyncFn(() =>
linkPackages({
repoDir: tmpRepoDir,
})
)
}
if (onlyPackages) {
return pkgPaths
}
}

let combinedDependencies = dependencies

if (onlyPackages) {
return pkgPaths
}
if (!(packageJson && packageJson.nextParamateSkipLocalDeps)) {
if (useTurbo) {
pkgPaths = await rootSpan
.traceChild('linkPackages')
.traceAsyncFn(() =>
linkPackages({
repoDir: useTurbo ? origRepoDir : tmpRepoDir,
})
)

if (onlyPackages) {
return pkgPaths
}
}

combinedDependencies = {
next: pkgPaths.get('next'),
...Object.keys(dependencies).reduce((prev, pkg) => {
Expand Down Expand Up @@ -178,6 +197,7 @@ async function createNextInstall({
tmpRepoDir,
}
}

return installDir
})
}
Expand Down
6 changes: 6 additions & 0 deletions test/lib/mock-trace.js
@@ -0,0 +1,6 @@
const mockTrace = () => ({
traceAsyncFn: (fn) => fn(mockTrace()),
traceChild: () => mockTrace(),
})

module.exports = { mockTrace }
6 changes: 5 additions & 1 deletion turbo.json
Expand Up @@ -31,13 +31,17 @@
},
"typescript": {},
"test-pack": {
"dependsOn": ["^test-pack", "test-pack-global-deps"],
"dependsOn": ["test-pack-global-deps", "test-pack-gitignored-deps"],
"outputs": ["packed-*.tgz"],
"env": ["NEXT_SWC_VERSION"]
},
"test-pack-global-deps": {
"inputs": ["../../scripts/test-pack-package.mts", "../../package.json"],
"cache": false
},
"test-pack-gitignored-deps": {
"inputs": ["dist/**"],
"cache": false
}
}
}