Skip to content

feat(turbopack): Print a warning about performance when starting with an invalidated cache #80631

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

Merged
merged 1 commit into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions crates/napi/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use turbo_tasks::{
message_queue::{CompilationEvent, Severity, TimingEvent},
trace::TraceRawVcs,
};
use turbo_tasks_backend::db_invalidation::invalidation_reasons;
use turbo_tasks_fs::{
DiskFileSystem, FileContent, FileSystem, FileSystemPath, get_relative_path_to,
util::uri_from_file,
Expand Down Expand Up @@ -571,9 +572,15 @@ pub async fn project_update(
pub async fn project_invalidate_persistent_cache(
#[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External<ProjectInstance>,
) -> napi::Result<()> {
tokio::task::spawn_blocking(move || project.turbo_tasks.invalidate_persistent_cache())
.await
.context("panicked while invalidating persistent cache")??;
tokio::task::spawn_blocking(move || {
// TODO: Let the JS caller specify a reason? We need to limit the reasons to ones we know
// how to generate a message for on the Rust side of the FFI.
project
.turbo_tasks
.invalidate_persistent_cache(invalidation_reasons::USER_REQUEST)
})
.await
.context("panicked while invalidating persistent cache")??;
Ok(())
}

Expand Down
88 changes: 62 additions & 26 deletions crates/napi/src/next_api/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ use serde::Serialize;
use tokio::sync::mpsc::Receiver;
use turbo_tasks::{
Effects, OperationVc, ReadRef, TaskId, TryJoinIterExt, TurboTasks, TurboTasksApi, UpdateInfo,
Vc, VcValueType, get_effects, message_queue::CompilationEvent,
task_statistics::TaskStatisticsApi, trace::TraceRawVcs,
Vc, VcValueType, get_effects,
message_queue::{CompilationEvent, Severity},
task_statistics::TaskStatisticsApi,
trace::TraceRawVcs,
};
use turbo_tasks_backend::{
DefaultBackingStorage, GitVersionInfo, NoopBackingStorage, default_backing_storage,
noop_backing_storage,
BackingStorage, DefaultBackingStorage, GitVersionInfo, NoopBackingStorage, StartupCacheState,
db_invalidation::invalidation_reasons, default_backing_storage, noop_backing_storage,
};
use turbo_tasks_fs::FileContent;
use turbopack_core::{
Expand Down Expand Up @@ -150,17 +152,51 @@ impl NextTurboTasks {
}
}

pub fn invalidate_persistent_cache(&self) -> Result<()> {
pub fn invalidate_persistent_cache(&self, reason_code: &str) -> Result<()> {
match self {
NextTurboTasks::Memory(_) => {}
NextTurboTasks::PersistentCaching(turbo_tasks) => {
turbo_tasks.backend().invalidate_storage()?
}
NextTurboTasks::PersistentCaching(turbo_tasks) => turbo_tasks
.backend()
.backing_storage()
.invalidate(reason_code)?,
}
Ok(())
}
}

#[derive(Serialize)]
struct StartupCacheInvalidationEvent {
reason_code: Option<String>,
}

impl CompilationEvent for StartupCacheInvalidationEvent {
fn type_name(&self) -> &'static str {
"StartupCacheInvalidationEvent"
}

fn severity(&self) -> Severity {
Severity::Warning
}

fn message(&self) -> String {
let reason_msg = match self.reason_code.as_deref() {
Some(invalidation_reasons::PANIC) => {
" because we previously detected an internal error in Turbopack"
}
Some(invalidation_reasons::USER_REQUEST) => " as the result of a user request",
_ => "", // ignore unknown reasons
};
format!(
"Turbopack's persistent cache has been deleted{reason_msg}. Builds or page loads may \
be slower as a result."
)
}

fn to_json(&self) -> String {
serde_json::to_string(self).unwrap()
}
}

pub fn create_turbo_tasks(
output_path: PathBuf,
persistent_caching: bool,
Expand All @@ -174,24 +210,24 @@ pub fn create_turbo_tasks(
dirty: option_env!("CI").is_none_or(|value| value.is_empty())
&& env!("VERGEN_GIT_DIRTY") == "true",
};
NextTurboTasks::PersistentCaching(TurboTasks::new(
turbo_tasks_backend::TurboTasksBackend::new(
turbo_tasks_backend::BackendOptions {
storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
turbo_tasks_backend::StorageMode::ReadOnly
} else {
turbo_tasks_backend::StorageMode::ReadWrite
}),
dependency_tracking,
..Default::default()
},
default_backing_storage(
&output_path.join("cache/turbopack"),
&version_info,
is_ci,
)?,
),
))
let (backing_storage, cache_state) =
default_backing_storage(&output_path.join("cache/turbopack"), &version_info, is_ci)?;
let tt = TurboTasks::new(turbo_tasks_backend::TurboTasksBackend::new(
turbo_tasks_backend::BackendOptions {
storage_mode: Some(if std::env::var("TURBO_ENGINE_READ_ONLY").is_ok() {
turbo_tasks_backend::StorageMode::ReadOnly
} else {
turbo_tasks_backend::StorageMode::ReadWrite
}),
dependency_tracking,
..Default::default()
},
backing_storage,
));
if let StartupCacheState::Invalidated { reason_code } = cache_state {
tt.send_compilation_event(Arc::new(StartupCacheInvalidationEvent { reason_code }));
}
NextTurboTasks::PersistentCaching(tt)
} else {
NextTurboTasks::Memory(TurboTasks::new(
turbo_tasks_backend::TurboTasksBackend::new(
Expand Down
5 changes: 3 additions & 2 deletions packages/next/src/build/swc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -712,13 +712,14 @@ function bindingToApi(
)
}

compilationEventsSubscribe() {
compilationEventsSubscribe(eventTypes?: string[]) {
return subscribe<TurbopackResult<CompilationEvent>>(
true,
async (callback) => {
binding.projectCompilationEventsSubscribe(
this._nativeProject,
callback
callback,
eventTypes
)
}
)
Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ export interface Project {
aggregationMs: number
): AsyncIterableIterator<TurbopackResult<UpdateMessage>>

compilationEventsSubscribe(): AsyncIterableIterator<
TurbopackResult<CompilationEvent>
>
compilationEventsSubscribe(
eventTypes?: string[]
): AsyncIterableIterator<TurbopackResult<CompilationEvent>>

invalidatePersistentCache(): Promise<void>

Expand Down
29 changes: 2 additions & 27 deletions packages/next/src/build/turbopack-build/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import loadConfig from '../../server/config'
import { hasCustomExportOutput } from '../../export/utils'
import { Telemetry } from '../../telemetry/storage'
import { setGlobal } from '../../trace'
import * as Log from '../output/log'
import { isCI } from '../../server/ci-info'
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'

export async function turbopackBuild(): Promise<{
duration: number
Expand Down Expand Up @@ -91,32 +91,7 @@ export async function turbopackBuild(): Promise<{
}
)
try {
;(async function logCompilationEvents() {
for await (const event of project.compilationEventsSubscribe()) {
switch (event.severity) {
case 'EVENT':
Log.event(event.message)
break
case 'TRACE':
Log.trace(event.message)
break
case 'INFO':
Log.info(event.message)
break
case 'WARNING':
Log.warn(event.message)
break
case 'ERROR':
Log.error(event.message)
break
case 'FATAL':
Log.error(event.message)
break
default:
break
}
}
})()
backgroundLogCompilationEvents(project)

// Write an empty file in a known location to signal this was built with Turbopack
await fs.writeFile(path.join(distDir, 'turbopack'), '')
Expand Down
4 changes: 4 additions & 0 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import { getDevOverlayFontMiddleware } from '../../next-devtools/server/font/get
import { devIndicatorServerState } from './dev-indicator-server-state'
import { getDisableDevIndicatorMiddleware } from '../../next-devtools/server/dev-indicator-middleware'
import { getRestartDevServerMiddleware } from '../../next-devtools/server/restart-dev-server-middleware'
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'
// import { getSupportedBrowsers } from '../../build/utils'

const wsServer = new ws.Server({ noServer: true })
Expand Down Expand Up @@ -248,6 +249,9 @@ export async function createHotReloaderTurbopack(
memoryLimit: opts.nextConfig.experimental?.turbopackMemoryLimit,
}
)
backgroundLogCompilationEvents(project, {
eventTypes: ['StartupCacheInvalidationEvent'],
})
setBundlerFindSourceMapImplementation(
getSourceMapFromTurbopack.bind(null, project, projectPath)
)
Expand Down
44 changes: 44 additions & 0 deletions packages/next/src/shared/lib/turbopack/compilation-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Project } from '../../../build/swc/types'
import * as Log from '../../../build/output/log'

/**
* Subscribes to compilation events for `project` and prints them using the
* `Log` library.
*
* The `signal` argument is partially implemented. The abort may not happen until the next
* compilation event arrives.
*/
export function backgroundLogCompilationEvents(
project: Project,
{ eventTypes, signal }: { eventTypes?: string[]; signal?: AbortSignal } = {}
) {
;(async function () {
for await (const event of project.compilationEventsSubscribe(eventTypes)) {
if (signal?.aborted) {
return
}
switch (event.severity) {
case 'EVENT':
Log.event(event.message)
break
case 'TRACE':
Log.trace(event.message)
break
case 'INFO':
Log.info(event.message)
break
case 'WARNING':
Log.warn(event.message)
break
case 'ERROR':
Log.error(event.message)
break
case 'FATAL':
Log.error(event.message)
break
default:
break
}
}
})()
}
2 changes: 1 addition & 1 deletion turbopack/crates/turbo-tasks-backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ rand = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
serde_path_to_error = { workspace = true }
smallvec = { workspace = true }
tokio = { workspace = true }
Expand All @@ -54,7 +55,6 @@ turbo-tasks-testing = { workspace = true }
[dev-dependencies]
criterion = { workspace = true, features = ["async_tokio"] }
regex = { workspace = true }
serde_json = { workspace = true }
tempfile = { workspace = true }
rstest = { workspace = true }

Expand Down
4 changes: 2 additions & 2 deletions turbopack/crates/turbo-tasks-backend/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ impl<B: BackingStorage> TurboTasksBackend<B> {
)))
}

pub fn invalidate_storage(&self) -> Result<()> {
self.0.backing_storage.invalidate()
pub fn backing_storage(&self) -> &B {
&self.0.backing_storage
}
}

Expand Down
41 changes: 30 additions & 11 deletions turbopack/crates/turbo-tasks-backend/src/backing_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,36 @@ use crate::{
utils::chunked_vec::ChunkedVec,
};

pub trait BackingStorage: 'static + Send + Sync {
/// Represents types accepted by [`TurboTasksBackend::new`]. Typically this is the value returned by
/// [`default_backing_storage`] or [`noop_backing_storage`].
///
/// This trait is [sealed]. External crates are not allowed to implement it.
///
/// [`default_backing_storage`]: crate::default_backing_storage
/// [`noop_backing_storage`]: crate::noop_backing_storage
/// [`TurboTasksBackend::new`]: crate::TurboTasksBackend::new
/// [sealed]: https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/
pub trait BackingStorage: BackingStorageSealed {
/// Called when the database should be invalidated upon re-initialization.
///
/// This typically means that we'll restart the process or `turbo-tasks` soon with a fresh
/// database. If this happens, there's no point in writing anything else to disk, or flushing
/// during [`KeyValueDatabase::shutdown`].
///
/// This can be implemented by calling [`invalidate_db`] with
/// the database's non-versioned base path.
///
/// [`KeyValueDatabase::shutdown`]: crate::database::key_value_database::KeyValueDatabase::shutdown
/// [`invalidate_db`]: crate::database::db_invalidation::invalidate_db
fn invalidate(&self, reason_code: &str) -> Result<()>;
}

/// Private methods used by [`BackingStorage`]. This trait is `pub` (because of the sealed-trait
/// pattern), but should not be exported outside of the crate.
///
/// [`BackingStorage`] is exported for documentation reasons and to expose the public
/// [`BackingStorage::invalidate`] method.
pub trait BackingStorageSealed: 'static + Send + Sync {
type ReadTransaction<'l>;
fn lower_read_transaction<'l: 'i + 'r, 'i: 'r, 'r>(
tx: &'r Self::ReadTransaction<'l>,
Expand Down Expand Up @@ -63,16 +92,6 @@ pub trait BackingStorage: 'static + Send + Sync {
category: TaskDataCategory,
) -> Result<Vec<CachedDataItem>>;

/// Called when the database should be invalidated upon re-initialization.
///
/// This typically means that we'll restart the process or `turbo-tasks` soon with a fresh
/// database. If this happens, there's no point in writing anything else to disk, or flushing
/// during [`KeyValueDatabase::shutdown`].
///
/// This can be implemented by calling [`crate::database::db_invalidation::invalidate_db`] with
/// the database's non-versioned base path.
fn invalidate(&self) -> Result<()>;

fn shutdown(&self) -> Result<()> {
Ok(())
}
Expand Down
Loading
Loading