Skip to content

Commit 7201319

Browse files
committed
perf: Cache compiled env wildcard regexes for builtin pass-through patterns
The ~30 builtin pass-through environment variable patterns (HOME, PATH, VSCODE_*, DOCKER_*, GITHUB_*, etc.) were recompiled into regexes on every env() call — 1690 times per run in a large monorepo. Now compiled once at TaskHasher construction and reused via CompiledWildcards. - Add CompiledWildcards type to turborepo-env that pre-compiles include/exclude wildcard patterns into reusable Regex objects - Add from_compiled_wildcards and pass_through_env_compiled methods that use pre-compiled regexes instead of recompiling - Move builtin pass-through list to BUILTIN_PASS_THROUGH_ENV constant in turborepo-env (was duplicated inline in task-hash) - Pre-compile at TaskHasher::new, reuse across all calculate_task_hash and env() calls
1 parent 92f39d4 commit 7201319

File tree

2 files changed

+292
-67
lines changed

2 files changed

+292
-67
lines changed

crates/turborepo-env/src/lib.rs

Lines changed: 277 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::{
88
ops::{Deref, DerefMut},
99
};
1010

11-
use regex::RegexBuilder;
11+
use regex::{Regex, RegexBuilder};
1212
use serde::Serialize;
1313
use sha2::{Digest, Sha256};
1414
use thiserror::Error;
@@ -17,6 +17,69 @@ pub mod platform;
1717

1818
const DEFAULT_ENV_VARS: &[&str] = ["VERCEL_ANALYTICS_ID", "VERCEL_TARGET_ENV"].as_slice();
1919

20+
pub const BUILTIN_PASS_THROUGH_ENV: &[&str] = &[
21+
"HOME",
22+
"USER",
23+
"TZ",
24+
"LANG",
25+
"SHELL",
26+
"PWD",
27+
"XDG_RUNTIME_DIR",
28+
"XAUTHORITY",
29+
"DBUS_SESSION_BUS_ADDRESS",
30+
"CI",
31+
"NODE_OPTIONS",
32+
"COREPACK_HOME",
33+
"LD_LIBRARY_PATH",
34+
"DYLD_FALLBACK_LIBRARY_PATH",
35+
"LIBPATH",
36+
"LD_PRELOAD",
37+
"DYLD_INSERT_LIBRARIES",
38+
"COLORTERM",
39+
"TERM",
40+
"TERM_PROGRAM",
41+
"DISPLAY",
42+
"TMP",
43+
"TEMP",
44+
// Windows
45+
"WINDIR",
46+
"ProgramFiles",
47+
"ProgramFiles(x86)",
48+
// VSCode IDE
49+
"VSCODE_*",
50+
"ELECTRON_RUN_AS_NODE",
51+
// Docker
52+
"DOCKER_*",
53+
"BUILDKIT_*",
54+
// Docker compose
55+
"COMPOSE_*",
56+
// Jetbrains IDE
57+
"JB_IDE_*",
58+
"JB_INTERPRETER",
59+
"_JETBRAINS_TEST_RUNNER_RUN_SCOPE_TYPE",
60+
// Vercel specific
61+
"VERCEL",
62+
"VERCEL_*",
63+
"NEXT_*",
64+
"USE_OUTPUT_FOR_EDGE_FUNCTIONS",
65+
"NOW_BUILDER",
66+
"VC_MICROFRONTENDS_CONFIG_FILE_NAME",
67+
// GitHub Actions
68+
"GITHUB_*",
69+
"RUNNER_*",
70+
// Command Prompt casing of env variables
71+
"APPDATA",
72+
"PATH",
73+
"PROGRAMDATA",
74+
"SYSTEMROOT",
75+
"SYSTEMDRIVE",
76+
"USERPROFILE",
77+
"HOMEDRIVE",
78+
"HOMEPATH",
79+
"PNPM_HOME",
80+
"NPM_CONFIG_STORE_DIR",
81+
];
82+
2083
#[derive(Clone, Debug, Error)]
2184
pub enum Error {
2285
#[error("Failed to parse regex: {0}")]
@@ -107,6 +170,60 @@ impl WildcardMaps {
107170
}
108171
}
109172

173+
/// Pre-compiled include/exclude wildcard regexes. Compile once and reuse
174+
/// across tasks that share the same wildcard patterns.
175+
pub struct CompiledWildcards {
176+
include_regex: Option<Regex>,
177+
exclude_regex: Option<Regex>,
178+
}
179+
180+
impl CompiledWildcards {
181+
pub fn compile(wildcard_patterns: &[impl AsRef<str>]) -> Result<Self, Error> {
182+
let mut include_patterns = Vec::new();
183+
let mut exclude_patterns = Vec::new();
184+
185+
for wildcard_pattern in wildcard_patterns {
186+
let wildcard_pattern = wildcard_pattern.as_ref();
187+
if let Some(rest) = wildcard_pattern.strip_prefix('!') {
188+
exclude_patterns.push(wildcard_to_regex_pattern(rest));
189+
} else if wildcard_pattern.starts_with("\\!") {
190+
include_patterns.push(wildcard_to_regex_pattern(&wildcard_pattern[1..]));
191+
} else {
192+
include_patterns.push(wildcard_to_regex_pattern(wildcard_pattern));
193+
}
194+
}
195+
196+
let case_insensitive = cfg!(windows);
197+
198+
let include_regex = if include_patterns.is_empty() {
199+
None
200+
} else {
201+
let pattern = format!("^({})$", include_patterns.join("|"));
202+
Some(
203+
RegexBuilder::new(&pattern)
204+
.case_insensitive(case_insensitive)
205+
.build()?,
206+
)
207+
};
208+
209+
let exclude_regex = if exclude_patterns.is_empty() {
210+
None
211+
} else {
212+
let pattern = format!("^({})$", exclude_patterns.join("|"));
213+
Some(
214+
RegexBuilder::new(&pattern)
215+
.case_insensitive(case_insensitive)
216+
.build()?,
217+
)
218+
};
219+
220+
Ok(CompiledWildcards {
221+
include_regex,
222+
exclude_regex,
223+
})
224+
}
225+
}
226+
110227
impl From<HashMap<String, String>> for EnvironmentVariableMap {
111228
fn from(map: HashMap<String, String>) -> Self {
112229
EnvironmentVariableMap(map)
@@ -285,6 +402,45 @@ impl EnvironmentVariableMap {
285402

286403
Ok(pass_through_env)
287404
}
405+
406+
/// Like `from_wildcards` but uses pre-compiled regexes.
407+
pub fn from_compiled_wildcards(&self, compiled: &CompiledWildcards) -> EnvironmentVariableMap {
408+
let mut output = EnvironmentVariableMap::default();
409+
for (env_var, env_value) in &self.0 {
410+
let included = compiled
411+
.include_regex
412+
.as_ref()
413+
.is_some_and(|re| re.is_match(env_var));
414+
let excluded = compiled
415+
.exclude_regex
416+
.as_ref()
417+
.is_some_and(|re| re.is_match(env_var));
418+
if included && !excluded {
419+
output.insert(env_var.clone(), env_value.clone());
420+
}
421+
}
422+
output
423+
}
424+
425+
/// Like `pass_through_env` but uses pre-compiled builtin wildcards.
426+
pub fn pass_through_env_compiled(
427+
&self,
428+
compiled_builtins: &CompiledWildcards,
429+
global_env: &Self,
430+
task_pass_through: &[impl AsRef<str>],
431+
) -> Result<Self, Error> {
432+
let default_env_var_pass_through_map = self.from_compiled_wildcards(compiled_builtins);
433+
let task_pass_through_env =
434+
self.wildcard_map_from_wildcards_unresolved(task_pass_through)?;
435+
436+
let mut pass_through_env = EnvironmentVariableMap::default();
437+
pass_through_env.union(&default_env_var_pass_through_map);
438+
pass_through_env.union(global_env);
439+
pass_through_env.union(&task_pass_through_env.inclusions);
440+
pass_through_env.difference(&task_pass_through_env.exclusions);
441+
442+
Ok(pass_through_env)
443+
}
288444
}
289445

290446
const WILDCARD: char = '*';
@@ -469,4 +625,124 @@ mod tests {
469625
actual.sort();
470626
assert_eq!(actual, expected);
471627
}
628+
629+
#[test_case(&["FOO*"], &["FOO", "FOOBAR", "FOOD", "PATH"] ; "folds 3 sources")]
630+
#[test_case(&["!FOO"], &["PATH"] ; "remove global")]
631+
#[test_case(&["!PATH"], &["FOO"] ; "remove builtin")]
632+
#[test_case(&["FOO*", "!FOOD"], &["FOO", "FOOBAR", "PATH"] ; "mixing negations")]
633+
fn test_pass_through_env_compiled_matches_original(task: &[&str], expected: &[&str]) {
634+
let env_at_start = EnvironmentVariableMap(
635+
vec![
636+
("PATH", "of"),
637+
("FOO", "bar"),
638+
("FOOBAR", "baz"),
639+
("FOOD", "cheese"),
640+
("BAR", "nuts"),
641+
]
642+
.into_iter()
643+
.map(|(k, v)| (k.to_owned(), v.to_owned()))
644+
.collect(),
645+
);
646+
let global_env = EnvironmentVariableMap(
647+
vec![("FOO", "bar")]
648+
.into_iter()
649+
.map(|(k, v)| (k.to_owned(), v.to_owned()))
650+
.collect(),
651+
);
652+
let builtins: &[&str] = &["PATH"];
653+
let compiled = CompiledWildcards::compile(builtins).unwrap();
654+
let output = env_at_start
655+
.pass_through_env_compiled(&compiled, &global_env, task)
656+
.unwrap();
657+
let mut actual: Vec<_> = output.keys().map(|s| s.as_str()).collect();
658+
actual.sort();
659+
assert_eq!(actual, expected);
660+
}
661+
662+
#[test]
663+
fn test_compiled_wildcards_matches_from_wildcards() {
664+
let env = EnvironmentVariableMap(
665+
vec![
666+
("HOME", "/home/user"),
667+
("PATH", "/usr/bin"),
668+
("VSCODE_PID", "12345"),
669+
("DOCKER_HOST", "tcp://localhost"),
670+
("GITHUB_TOKEN", "ghp_xxx"),
671+
("NEXT_PUBLIC_API", "https://api"),
672+
("RANDOM_VAR", "value"),
673+
("CI", "true"),
674+
("VERCEL", "1"),
675+
("VERCEL_URL", "example.vercel.app"),
676+
]
677+
.into_iter()
678+
.map(|(k, v)| (k.to_owned(), v.to_owned()))
679+
.collect(),
680+
);
681+
682+
let original = env.from_wildcards(BUILTIN_PASS_THROUGH_ENV).unwrap();
683+
let compiled = CompiledWildcards::compile(BUILTIN_PASS_THROUGH_ENV).unwrap();
684+
let from_compiled = env.from_compiled_wildcards(&compiled);
685+
686+
let mut orig_keys: Vec<_> = original.keys().cloned().collect();
687+
let mut comp_keys: Vec<_> = from_compiled.keys().cloned().collect();
688+
orig_keys.sort();
689+
comp_keys.sort();
690+
691+
assert_eq!(
692+
orig_keys, comp_keys,
693+
"compiled and original wildcard matching must produce identical keys"
694+
);
695+
696+
for key in &orig_keys {
697+
assert_eq!(
698+
original.get(key),
699+
from_compiled.get(key),
700+
"values differ for key {key}"
701+
);
702+
}
703+
}
704+
705+
#[test]
706+
fn test_builtin_pass_through_env_compiles() {
707+
CompiledWildcards::compile(BUILTIN_PASS_THROUGH_ENV)
708+
.expect("BUILTIN_PASS_THROUGH_ENV should compile without error");
709+
}
710+
711+
#[test]
712+
fn test_compiled_wildcards_with_excludes() {
713+
let env = EnvironmentVariableMap(
714+
vec![("FOO", "1"), ("FOOBAR", "2"), ("FOOD", "3"), ("BAR", "4")]
715+
.into_iter()
716+
.map(|(k, v)| (k.to_owned(), v.to_owned()))
717+
.collect(),
718+
);
719+
720+
let patterns: &[&str] = &["FOO*", "!FOOD"];
721+
let original = env.from_wildcards(patterns).unwrap();
722+
let compiled = CompiledWildcards::compile(patterns).unwrap();
723+
let from_compiled = env.from_compiled_wildcards(&compiled);
724+
725+
let mut orig_keys: Vec<_> = original.keys().cloned().collect();
726+
let mut comp_keys: Vec<_> = from_compiled.keys().cloned().collect();
727+
orig_keys.sort();
728+
comp_keys.sort();
729+
730+
assert_eq!(orig_keys, comp_keys);
731+
assert_eq!(orig_keys, vec!["FOO", "FOOBAR"]);
732+
}
733+
734+
#[test]
735+
fn test_compiled_wildcards_empty_patterns() {
736+
let env = EnvironmentVariableMap(
737+
vec![("FOO", "bar")]
738+
.into_iter()
739+
.map(|(k, v)| (k.to_owned(), v.to_owned()))
740+
.collect(),
741+
);
742+
743+
let empty: &[&str] = &[];
744+
let compiled = CompiledWildcards::compile(empty).unwrap();
745+
let result = env.from_compiled_wildcards(&compiled);
746+
assert!(result.is_empty(), "empty patterns should match nothing");
747+
}
472748
}

0 commit comments

Comments
 (0)