Skip to content

Commit

Permalink
Turbopack: limit build concurrency, show progress bar (#62319)
Browse files Browse the repository at this point in the history
### What?

Run 10 concurrent Turbopack compilation jobs instead of all of them
show a progress spinner

### Why?

* In future this allows to reclaim memory for finished jobs
* It looks nicer in tracing


Closes PACK-2561
  • Loading branch information
sokra committed Feb 21, 2024
1 parent 8421a97 commit 46521db
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 103 deletions.
29 changes: 25 additions & 4 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { defaultConfig } from '../server/config-shared'
import devalue from 'next/dist/compiled/devalue'
import findUp from 'next/dist/compiled/find-up'
import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import { Sema } from 'next/dist/compiled/async-sema'
import path from 'path'
import {
STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
Expand Down Expand Up @@ -177,6 +178,7 @@ import {
import { TurbopackManifestLoader } from '../server/dev/turbopack/manifest-loader'
import type { Entrypoints } from '../server/dev/turbopack/types'
import { buildCustomRoute } from '../lib/build-custom-route'
import { createProgress } from './progress'

interface ExperimentalBypassForInfo {
experimentalBypassFor?: RouteHas[]
Expand Down Expand Up @@ -1416,9 +1418,28 @@ export default async function build(
rewrites: emptyRewritesObjToBeImplemented,
})

const promises = []
for (const [page, route] of currentEntrypoints.page) {
const progress = createProgress(
currentEntrypoints.page.size + currentEntrypoints.app.size + 1,
'Building'
)
const promises: Promise<any>[] = []
const sema = new Sema(10)
const enqueue = (fn: () => Promise<void>) => {
promises.push(
(async () => {
await sema.acquire()
try {
await fn()
} finally {
sema.release()
progress()
}
})()
)
}

for (const [page, route] of currentEntrypoints.page) {
enqueue(() =>
handleRouteType({
dev,
page,
Expand All @@ -1434,7 +1455,7 @@ export default async function build(
}

for (const [page, route] of currentEntrypoints.app) {
promises.push(
enqueue(() =>
handleRouteType({
page,
dev: false,
Expand All @@ -1448,7 +1469,7 @@ export default async function build(
)
}

promises.push(
enqueue(() =>
handlePagesErrorRoute({
currentIssues,
entrypoints: currentEntrypoints,
Expand Down
86 changes: 86 additions & 0 deletions packages/next/src/build/progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as Log from '../build/output/log'
import createSpinner from './spinner'

function divideSegments(number: number, segments: number): number[] {
const result = []
while (number > 0 && segments > 0) {
const dividedNumber =
number < segments ? number : Math.floor(number / segments)

number -= dividedNumber
segments--
result.push(dividedNumber)
}
return result
}

export const createProgress = (total: number, label: string) => {
const segments = divideSegments(total, 4)

if (total === 0) {
throw new Error('invariant: progress total can not be zero')
}
let currentSegmentTotal = segments.shift()
let currentSegmentCount = 0
let lastProgressOutput = Date.now()
let curProgress = 0
let progressSpinner = createSpinner(`${label} (${curProgress}/${total})`, {
spinner: {
frames: [
'[ ]',
'[= ]',
'[== ]',
'[=== ]',
'[ ===]',
'[ ==]',
'[ =]',
'[ ]',
'[ =]',
'[ ==]',
'[ ===]',
'[====]',
'[=== ]',
'[== ]',
'[= ]',
],
interval: 200,
},
})

return () => {
curProgress++

// Make sure we only log once
// - per fully generated segment, or
// - per minute
// when not showing the spinner
if (!progressSpinner) {
currentSegmentCount++

if (currentSegmentCount === currentSegmentTotal) {
currentSegmentTotal = segments.shift()
currentSegmentCount = 0
} else if (lastProgressOutput + 60000 > Date.now()) {
return
}

lastProgressOutput = Date.now()
}

const isFinished = curProgress === total
if (progressSpinner && !isFinished) {
progressSpinner.setText(`${label} (${curProgress}/${total})`)
} else {
if (progressSpinner) {
progressSpinner.stop()
}
console.log(
` ${
isFinished ? Log.prefixes.event : Log.prefixes.info
} ${label} (${curProgress}/${total}) ${
isFinished ? '' : process.stdout.isTTY ? '\n' : '\r'
}`
)
}
}
}
26 changes: 15 additions & 11 deletions packages/next/src/build/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@ export default function createSpinner(
options: ora.Options = {},
logFn: (...data: any[]) => void = console.log
) {
let spinner: undefined | ora.Ora
let spinner: undefined | (ora.Ora & { setText: (text: string) => void })

const prefixText = ` ${Log.prefixes.info} ${text} `
// Add \r at beginning to reset the current line of loading status text
const suffixText = `\r ${Log.prefixes.event} ${text} `
let prefixText = ` ${Log.prefixes.info} ${text} `

if (process.stdout.isTTY) {
spinner = ora({
Expand All @@ -24,7 +22,7 @@ export default function createSpinner(
spinner: dotsSpinner,
stream: process.stdout,
...options,
}).start()
}).start() as ora.Ora & { setText: (text: string) => void }

// Add capturing of console.log/warn/error to allow pausing
// the spinner before logging and then restarting spinner after
Expand All @@ -49,18 +47,24 @@ export default function createSpinner(
console.warn = origWarn
console.error = origError
}
spinner.setText = (newText: string) => {
text = newText
prefixText = ` ${Log.prefixes.info} ${newText} `
spinner!.prefixText = prefixText
return spinner!
}
spinner.stop = (): ora.Ora => {
origStop()
resetLog()
return spinner!
}
spinner.stopAndPersist = (): ora.Ora => {
if (suffixText) {
if (spinner) {
spinner.text = suffixText
} else {
logFn(suffixText)
}
// Add \r at beginning to reset the current line of loading status text
const suffixText = `\r ${Log.prefixes.event} ${text} `
if (spinner) {
spinner.text = suffixText
} else {
logFn(suffixText)
}
origStopAndPersist()
resetLog()
Expand Down
89 changes: 1 addition & 88 deletions packages/next/src/export/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { dirname, join, resolve, sep } from 'path'
import { formatAmpMessages } from '../build/output/index'
import type { AmpPageStatus } from '../build/output/index'
import * as Log from '../build/output/log'
import createSpinner from '../build/spinner'
import { RSC_SUFFIX, SSG_FALLBACK_EXPORT_ERROR } from '../lib/constants'
import { recursiveCopy } from '../lib/recursive-copy'
import {
Expand Down Expand Up @@ -56,93 +55,7 @@ import { needsExperimentalReact } from '../lib/needs-experimental-react'
import { formatManifest } from '../build/manifests/formatter/format-manifest'
import { validateRevalidate } from '../server/lib/patch-fetch'
import { TurborepoAccessTraceResult } from '../build/turborepo-access-trace'

function divideSegments(number: number, segments: number): number[] {
const result = []
while (number > 0 && segments > 0) {
const dividedNumber =
number < segments ? number : Math.floor(number / segments)

number -= dividedNumber
segments--
result.push(dividedNumber)
}
return result
}

const createProgress = (total: number, label: string) => {
const segments = divideSegments(total, 4)

if (total === 0) {
throw new Error('invariant: progress total can not be zero')
}
let currentSegmentTotal = segments.shift()
let currentSegmentCount = 0
let lastProgressOutput = Date.now()
let curProgress = 0
let progressSpinner = createSpinner(`${label} (${curProgress}/${total})`, {
spinner: {
frames: [
'[ ]',
'[= ]',
'[== ]',
'[=== ]',
'[ ===]',
'[ ==]',
'[ =]',
'[ ]',
'[ =]',
'[ ==]',
'[ ===]',
'[====]',
'[=== ]',
'[== ]',
'[= ]',
],
interval: 200,
},
})

return () => {
curProgress++

// Make sure we only log once
// - per fully generated segment, or
// - per minute
// when not showing the spinner
if (!progressSpinner) {
currentSegmentCount++

if (currentSegmentCount === currentSegmentTotal) {
currentSegmentTotal = segments.shift()
currentSegmentCount = 0
} else if (lastProgressOutput + 60000 > Date.now()) {
return
}

lastProgressOutput = Date.now()
}

const isFinished = curProgress === total
// Use \r to reset current line with spinner.
// If it's 100% progressed, then we don't need to break a new line to avoid logging from routes while building.
const newText = `\r ${
isFinished ? Log.prefixes.event : Log.prefixes.info
} ${label} (${curProgress}/${total}) ${
isFinished ? '' : process.stdout.isTTY ? '\n' : '\r'
}`
if (progressSpinner) {
progressSpinner.text = newText
} else {
console.log(newText)
}

if (isFinished && progressSpinner) {
progressSpinner.stop()
console.log(newText)
}
}
}
import { createProgress } from '../build/progress'

export class ExportError extends Error {
code = 'NEXT_EXPORT_ERROR'
Expand Down

0 comments on commit 46521db

Please sign in to comment.