Skip to content

Commit

Permalink
feat(turbopack): Add an env var to debug-print the fast refresh inval…
Browse files Browse the repository at this point in the history
…idation reason (#72296)

Setting the environment variable when building (`next dev`)

```
NEXT_TURBOPACK_INCLUDE_UPDATE_REASONS=1
```

causes `[Update Reasons]` messages to be logged to the terminal console (not in the browser) when a file is changed:

![Screenshot 2024-11-04 at 4.31.16 PM.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/HAZVitxRNnZz8QMiPn4a/1af5a4f0-cfba-4ca8-b188-d6be2064b0dc.png)

These messages will not appear if no reason was recorded when the invalidation happened, so this provided on a best-effort basis.

**Why?** The hope is that this can help debug cases where users get stuck in a fast refresh update loop: https://vercel.slack.com/archives/C03S8ED1DKM/p1730737898652749

Closes PACK-3374
  • Loading branch information
bgw authored Nov 5, 2024
1 parent fdcc3c9 commit 8abe1fd
Show file tree
Hide file tree
Showing 6 changed files with 63 additions and 19 deletions.
45 changes: 34 additions & 11 deletions crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,10 @@ pub fn project_hmr_identifiers_subscribe(
)
}

struct NapiUpdateInfoOpts {
include_reasons: bool,
}

enum UpdateMessage {
Start,
End(UpdateInfo),
Expand All @@ -904,16 +908,16 @@ struct NapiUpdateMessage {
pub value: Option<NapiUpdateInfo>,
}

impl From<UpdateMessage> for NapiUpdateMessage {
fn from(update_message: UpdateMessage) -> Self {
impl NapiUpdateMessage {
fn from_update_message(update_message: UpdateMessage, opts: NapiUpdateInfoOpts) -> Self {
match update_message {
UpdateMessage::Start => NapiUpdateMessage {
update_type: "start".to_string(),
value: None,
},
UpdateMessage::End(info) => NapiUpdateMessage {
update_type: "end".to_string(),
value: Some(info.into()),
value: Some(NapiUpdateInfo::from_update_info(info, opts)),
},
}
}
Expand All @@ -923,13 +927,27 @@ impl From<UpdateMessage> for NapiUpdateMessage {
struct NapiUpdateInfo {
pub duration: u32,
pub tasks: u32,
/// A human-readable list of invalidation reasons (typically changed file paths) if known. Will
/// be `None` if [`NapiUpdateInfoOpts::include_reasons`] is `false` or if no reason was
/// specified (not every invalidation includes a reason).
pub reasons: Option<String>,
}

impl From<UpdateInfo> for NapiUpdateInfo {
fn from(update_info: UpdateInfo) -> Self {
impl NapiUpdateInfo {
fn from_update_info(update_info: UpdateInfo, opts: NapiUpdateInfoOpts) -> Self {
Self {
duration: update_info.duration.as_millis() as u32,
tasks: update_info.tasks as u32,
// u32::MAX in milliseconds is 49.71 days
duration: update_info
.duration
.as_millis()
.try_into()
.expect("update duration in milliseconds should not exceed u32::MAX"),
tasks: update_info
.tasks
.try_into()
.expect("number of tasks should not exceed u32::MAX"),
reasons: (opts.include_reasons && !update_info.reasons.is_empty())
.then(|| update_info.reasons.to_string()),
}
}
}
Expand All @@ -949,12 +967,17 @@ impl From<UpdateInfo> for NapiUpdateInfo {
pub fn project_update_info_subscribe(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
aggregation_ms: u32,
include_reasons: bool,
func: JsFunction,
) -> napi::Result<()> {
let func: ThreadsafeFunction<UpdateMessage> = func.create_threadsafe_function(0, |ctx| {
let message = ctx.value;
Ok(vec![NapiUpdateMessage::from(message)])
})?;
let func: ThreadsafeFunction<UpdateMessage> =
func.create_threadsafe_function(0, move |ctx| {
let message = ctx.value;
Ok(vec![NapiUpdateMessage::from_update_message(
message,
NapiUpdateInfoOpts { include_reasons },
)])
})?;
let turbo_tasks = project.turbo_tasks.clone();
tokio::spawn(async move {
loop {
Expand Down
7 changes: 7 additions & 0 deletions packages/next/src/build/swc/generated-native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ export interface NapiUpdateMessage {
export interface NapiUpdateInfo {
duration: number
tasks: number
/**
* A human-readable list of invalidation reasons (typically changed file paths) if known. Will
* be `None` if [`NapiUpdateInfoOpts::include_reasons`] is `false` or if no reason was
* specified (not every invalidation includes a reason).
*/
reasons?: string
}
/**
* Subscribes to lifecycle events of the compilation.
Expand All @@ -263,6 +269,7 @@ export interface NapiUpdateInfo {
export function projectUpdateInfoSubscribe(
project: { __napiType: 'Project' },
aggregationMs: number,
includeReasons: boolean,
func: (...args: any[]) => any
): void
export interface StackFrame {
Expand Down
6 changes: 4 additions & 2 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type {
TurbopackResult,
TurbopackStackFrame,
Update,
UpdateInfoOpts,
UpdateMessage,
WrittenEndpoint,
} from './types'
Expand Down Expand Up @@ -761,11 +762,12 @@ function bindingToApi(
return binding.projectGetSourceMap(this._nativeProject, filePath)
}

updateInfoSubscribe(aggregationMs: number) {
updateInfoSubscribe(opts: UpdateInfoOpts) {
return subscribe<TurbopackResult<UpdateMessage>>(true, async (callback) =>
binding.projectUpdateInfoSubscribe(
this._nativeProject,
aggregationMs,
opts.aggregationMs,
opts.includeReasons ?? false,
callback
)
)
Expand Down
11 changes: 7 additions & 4 deletions packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import type {
ExternalObject,
NextTurboTasks,
RefCell,
NapiUpdateInfo as UpdateInfo,
} from './generated-native'

export type { NapiUpdateInfo as UpdateInfo } from './generated-native'

export interface Binding {
isWasm: boolean
turbo: {
Expand Down Expand Up @@ -195,9 +198,9 @@ export type UpdateMessage =
value: UpdateInfo
}

export interface UpdateInfo {
duration: number
tasks: number
export interface UpdateInfoOpts {
aggregationMs: number
includeReasons?: boolean
}

export interface Project {
Expand All @@ -220,7 +223,7 @@ export interface Project {
): Promise<TurbopackStackFrame | null>

updateInfoSubscribe(
aggregationMs: number
opts?: UpdateInfoOpts
): AsyncIterableIterator<TurbopackResult<UpdateMessage>>

shutdown(): Promise<void>
Expand Down
11 changes: 10 additions & 1 deletion packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const isTestMode = !!(
process.env.__NEXT_TEST_MODE ||
process.env.DEBUG
)
const includeUpdateReasons = !!process.env.NEXT_TURBOPACK_INCLUDE_UPDATE_REASONS

const sessionId = Math.floor(Number.MAX_SAFE_INTEGER * Math.random())

Expand Down Expand Up @@ -1018,13 +1019,21 @@ export async function createHotReloaderTurbopack(
})

async function handleProjectUpdates() {
for await (const updateMessage of project.updateInfoSubscribe(30)) {
for await (const updateMessage of project.updateInfoSubscribe({
aggregationMs: 30,
includeReasons: includeUpdateReasons,
})) {
switch (updateMessage.updateType) {
case 'start': {
hotReloader.send({ action: HMR_ACTIONS_SENT_TO_BROWSER.BUILDING })
break
}
case 'end': {
if (updateMessage.value.reasons) {
// debug: only populated when `process.env.NEXT_TURBOPACK_INCLUDE_UPDATE_REASONS` is set
// and a reason was supplied when the invalidation happened (not always true)
console.log('[Update Reasons]', updateMessage.value.reasons)
}
sendEnqueuedMessages()

function addErrors(
Expand Down
2 changes: 1 addition & 1 deletion test/development/basic/next-rs-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('next.rs api', () => {
browserslistQuery: 'last 2 versions',
})
projectUpdateSubscription = filterMapAsyncIterator(
project.updateInfoSubscribe(1000),
project.updateInfoSubscribe({ aggregationMs: 1000 }),
(update) => (update.updateType === 'end' ? update.value : undefined)
)
})
Expand Down

0 comments on commit 8abe1fd

Please sign in to comment.