Skip to content

Commit f41441a

Browse files
authored
Durable use cache: optimize env var existence checks (#98504)
We now separately track code that accesses non-inlined env vars at runtime in two categories - (existing) actual reads, the full value is accessed - (new) only unset/falsy/truthy is read at runtime For the second case, we only need to invalidate use-cache functions when the given env var transitioned betwen unset/falsy/truthy. But transitioning between two different truthy values doesn't caused invalidation.
1 parent 30d6d8f commit f41441a

58 files changed

Lines changed: 852 additions & 138 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/next-api/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ byteorder = { workspace = true }
2525
either = { workspace = true }
2626
futures = { workspace = true }
2727
indexmap = { workspace = true }
28+
itertools = { workspace = true }
2829
next-core = { workspace = true }
2930
regex = { workspace = true }
3031
roaring = { workspace = true }

‎crates/next-api/src/server_actions.rs‎

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use std::{borrow::Cow, collections::BTreeMap, io::Write, sync::LazyLock};
22

33
use anyhow::{Context, Result, bail};
44
use bincode::{Decode, Encode};
5+
use either::Either;
6+
use itertools::Itertools;
57
use next_core::{
68
get_next_package,
79
next_client_reference::{CssClientReferenceModule, EcmascriptClientReferenceModule},
@@ -48,7 +50,7 @@ use turbopack_core::{
4850
virtual_source::VirtualSource,
4951
};
5052
use turbopack_ecmascript::{
51-
EcmascriptAnalyzable, EcmascriptParsable, EnvVarInfo,
53+
EcmascriptAnalyzable, EcmascriptParsable, EnvVarAccessMode, EnvVarInfo,
5254
chunk::{EcmascriptChunkItem, EcmascriptChunkItemExt, EcmascriptChunkPlaceable},
5355
module_fragments::part::module::EcmascriptModulePartAsset,
5456
parse::ParseResult,
@@ -353,7 +355,8 @@ impl Asset for ServerActionManifestAsset {
353355
.await?,
354356
durability: data.as_ref().map(|d| ActionManifestWorkerEntryDurability {
355357
code_hash: d.ident_code_hash.as_str(),
356-
runtime_env_vars: d.runtime_env_vars.as_slice(),
358+
runtime_env_vars_read: d.runtime_env_vars_read.as_slice(),
359+
runtime_env_vars_existence: d.runtime_env_vars_existence.as_slice(),
357360
}),
358361
},
359362
);
@@ -405,10 +408,13 @@ pub async fn to_rsc_context(
405408
#[turbo_tasks::value]
406409
#[derive(Debug)]
407410
struct ModulesInformation {
408-
/// The combined code hash of all modules in the subgraph
411+
/// The combined code hash of all modules in the subgraph.
409412
pub ident_code_hash: RcStr,
410-
/// The merged and deduplicated list of all runtime env vars referenced in the subgraph
411-
pub runtime_env_vars: Vec<RcStr>,
413+
/// The merged and deduplicated list of all runtime env vars read in the subgraph.
414+
pub runtime_env_vars_read: Vec<RcStr>,
415+
/// The merged and deduplicated list of runtime env vars used only checked for set/falsy/truthy
416+
/// in the subgraph.
417+
pub runtime_env_vars_existence: Vec<RcStr>,
412418
}
413419

414420
#[turbo_tasks::function]
@@ -457,6 +463,24 @@ async fn compute_subtree_content_hash(
457463
/* include_traced */ true,
458464
)?;
459465

466+
let data = modules
467+
.into_iter()
468+
.map(async |m| {
469+
Ok((
470+
m,
471+
module_hash(
472+
*module_graph,
473+
chunking_context,
474+
async_module_info,
475+
*m,
476+
hash_salt,
477+
)
478+
.await?,
479+
))
480+
})
481+
.try_join()
482+
.await?;
483+
460484
static PRINT_USE_CACHE_SUBTREE: LazyLock<bool> = LazyLock::new(|| {
461485
std::env::var_os("TURBOPACK_PRINT_USE_CACHE_SUBTREE")
462486
.is_some_and(|v| v == "1" || v == "true")
@@ -465,24 +489,25 @@ async fn compute_subtree_content_hash(
465489
println!(
466490
"Modules in subtree for {}:\n{}",
467491
entry.ident().await?.path,
468-
modules
469-
.iter()
470-
.map(async |m| {
471-
let data = module_hash(
472-
*module_graph,
473-
chunking_context,
474-
async_module_info,
475-
**m,
476-
hash_salt,
477-
)
478-
.await?;
492+
data.iter()
493+
.map(async |(m, data)| {
479494
Ok(format!(
480495
" '{}': {} with env: {}",
481496
m.ident_string().await?,
482497
data.ident_code_hash,
483498
data.env_var_info
484499
.as_ref()
485-
.map(|e| e.runtime.clone())
500+
.map(|e| {
501+
e.runtime
502+
.iter()
503+
.map(|(name, mode)| match mode {
504+
EnvVarAccessMode::Read => format!("read {name}"),
505+
EnvVarAccessMode::Existence => {
506+
format!("exists {name}")
507+
}
508+
})
509+
.collect::<Vec<_>>()
510+
})
486511
.unwrap_or_default()
487512
.join(",")
488513
))
@@ -493,40 +518,44 @@ async fn compute_subtree_content_hash(
493518
);
494519
}
495520

496-
let data = modules
497-
.into_iter()
498-
.map(async |m| {
499-
Ok((
500-
m,
501-
module_hash(
502-
*module_graph,
503-
chunking_context,
504-
async_module_info,
505-
*m,
506-
hash_salt,
507-
)
508-
.await?,
509-
))
510-
})
511-
.try_join()
512-
.await?;
513-
514521
let mut hashes = Vec::with_capacity(data.len());
515-
let mut runtime_env_vars = FxIndexSet::default();
522+
let mut runtime_env_vars = FxIndexMap::default();
516523

517524
for (_m, data) in &data {
518525
hashes.push(&data.ident_code_hash);
519526
if let Some(env) = &data.env_var_info {
520-
runtime_env_vars.extend(env.runtime.iter());
527+
for (name, mode) in &env.runtime {
528+
match mode {
529+
EnvVarAccessMode::Read => {
530+
// Overwrite
531+
runtime_env_vars.insert(name.clone(), EnvVarAccessMode::Read);
532+
}
533+
EnvVarAccessMode::Existence => {
534+
// Keep EnvVarAccessMode::Read if it already exists
535+
runtime_env_vars
536+
.entry(name.clone())
537+
.or_insert(EnvVarAccessMode::Existence);
538+
}
539+
}
540+
}
521541
}
522542
}
523543

524544
let hash = deterministic_hash("", hashes, HashAlgorithm::Xxh3Hash128Hex).into();
545+
let (runtime_read, runtime_existence): (Vec<_>, Vec<_>) =
546+
runtime_env_vars.into_iter().partition_map(|(name, mode)| {
547+
if mode == EnvVarAccessMode::Read {
548+
Either::Left(name)
549+
} else {
550+
Either::Right(name)
551+
}
552+
});
525553

526554
anyhow::Ok(
527555
ModulesInformation {
528556
ident_code_hash: hash,
529-
runtime_env_vars: runtime_env_vars.into_iter().cloned().collect(),
557+
runtime_env_vars_read: runtime_read,
558+
runtime_env_vars_existence: runtime_existence,
530559
}
531560
.cell(),
532561
)

‎crates/next-core/src/next_manifests/mod.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,8 @@ pub struct ActionManifestWorkerEntry<'a> {
472472
#[serde(rename_all = "camelCase")]
473473
pub struct ActionManifestWorkerEntryDurability<'a> {
474474
pub code_hash: &'a str,
475-
pub runtime_env_vars: &'a [RcStr],
475+
pub runtime_env_vars_read: &'a [RcStr],
476+
pub runtime_env_vars_existence: &'a [RcStr],
476477
}
477478

478479
#[derive(Serialize, Debug, Clone)]

‎packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ type Actions = {
7474
async: boolean
7575
durability?: {
7676
codeHash: string
77-
runtimeEnvVars: string[]
77+
runtimeEnvVarsRead: string[]
78+
runtimeEnvVarsExistence: string[]
7879
}
7980
}
8081
}

‎packages/next/src/server/app-render/manifests-singleton.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,8 @@ function createServerModuleMap(): ServerModuleMap {
375375
async: boolean
376376
durability?: {
377377
codeHash: string
378-
runtimeEnvVars: readonly string[]
378+
runtimeEnvVarsRead: readonly string[]
379+
runtimeEnvVarsExistence: readonly string[]
379380
}
380381
}
381382
| undefined

‎packages/next/src/server/use-cache/use-cache-wrapper.ts‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3675,12 +3675,11 @@ export async function cache(
36753675
* This returns a cache key that has to cover everything that can affect the result of the cached
36763676
* function (apart from the arguments). So
36773677
* - codeHash: the code itself that generates the return value
3678-
* - Notably, this excludes the following modules. Those are included via the Next.js version
3679-
* anyway:
3678+
* - Notably, this excludes the following modules. Those are included via the Next.js version anyway:
36803679
* - react, react-dom, private-next-rsc-server-reference, private-next-rsc-cache-wrapper
3681-
* - runtimeEnvVars: the keys and values runtime environment variables that the code reads (and are
3682-
* not inlined)
3683-
* - the version of Next.js (to account for RSC wire format changes, or use-cache-wrapper.ts
3680+
* - runtimeEnvVarsRead: the keys and values of runtime environment variables that the code reads
3681+
* - runtimeEnvVarExistence: the unset/falsy/truthy state of runtime environment variables used in boolean contexts
3682+
* - the version of Next.js (to account for RSC wire format changes, or use-cache-wrapper.ts changes)
36843683
*
36853684
* In case that granular information isn't available, fall back to
36863685
* buildId/deploymentId/hmrRefreshHash, which is a correct hash but over-invalidates way too often.
@@ -3702,16 +3701,21 @@ async function computeCacheKeyImplementationPart(
37023701
// Hash the env var values, to not leak secrets into the cache key.
37033702
.createHash('sha256')
37043703
.update(
3705-
durability.runtimeEnvVars
3706-
.map((k) => {
3704+
[
3705+
...durability.runtimeEnvVarsRead.map((k) => {
37073706
// Make sure not to stringify `undefined` and `"undefined"` to the same value.
3708-
return process.env[k] != null ? `${k}=${process.env[k]}` : k
3709-
})
3710-
.join('\0') ?? ''
3707+
return process.env[k] != null ? `${k}==${process.env[k]}` : k
3708+
}),
3709+
...durability.runtimeEnvVarsExistence.map((k) => {
3710+
const value = process.env[k]
3711+
const state = value == null ? 'unset' : value ? 'truthy' : 'falsy'
3712+
return `${k}~=${state}`
3713+
}),
3714+
].join('\0') ?? ''
37113715
)
37123716
.digest('hex')
37133717

3714-
// When more accurate analysis information is available, use codeHash + runtimeEnvVars
3718+
// When more accurate analysis information is available, use codeHash + runtime env vars
37153719
return [durability.codeHash, nextVersion, runtimeEnvVarStateHash]
37163720
} else {
37173721
// Because the Action ID is not yet unique per implementation of that Action we can't
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
async function logic() {
2+
'use cache'
3+
if (!process.env.FOO || !process.env.BAR) {
4+
return true
5+
}
6+
return false
7+
}
8+
9+
export default async function Page() {
10+
const value = await logic()
11+
return <p>{value}</p>
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Image from 'next/image'
2+
3+
async function logic() {
4+
'use cache'
5+
return Date.now()
6+
}
7+
8+
export default async function Page() {
9+
const value = await logic()
10+
return (
11+
<div>
12+
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
13+
<p>{value}</p>
14+
</div>
15+
)
16+
}

‎test/production/app-dir/use-cache-code-hash/next.config.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
*/
44
const nextConfig = {
55
cacheComponents: true,
6+
supportsImmutableAssets: true,
67
experimental: {
78
durableUseCacheEntries: true,
9+
runtimeServerDeploymentId: true,
810
},
911
}
1012

0 commit comments

Comments
 (0)