LibJS: Use foldhash in parser and scope-collector hash maps

The std default RandomState (SipHash) was using ~9 percentage points
of CPU on hash_one and write across the parse hot path, with the
string interner adding another ~3 pp on top. The cost was spread
across the interner, the scope collector's IndexMap<Utf16String, _>,
and several parser-side HashSet<Utf16String> declarations.

Use foldhash::quality::RandomState for the parser, scope collector,
and string interner via a new fast_hash module. Quality keeps
HashDoS resistance (keys are lexer tokens, attacker-controlled in a
browser context) while shedding SipHash's per-byte cost, and on this
workload it benchmarks slightly faster than foldhash::fast.
This commit is contained in:
Andreas Kling 2026-05-05 00:14:33 +02:00 committed by Andreas Kling
parent 17d3a285a7
commit d9dd412440
9 changed files with 39 additions and 22 deletions

1
Cargo.lock generated
View file

@ -375,6 +375,7 @@ version = "0.1.0"
dependencies = [
"bytecode_def",
"cbindgen",
"foldhash",
"indexmap",
"libunicode_rust",
"num-bigint",

View file

@ -14,6 +14,7 @@ num-bigint = "0.4"
num-traits = "0.2"
num-integer = "0.1"
indexmap = "2"
foldhash = "0.1"
[build-dependencies]
bytecode_def = { path = "../BytecodeDef" }

View file

@ -28,7 +28,7 @@ use std::ops::{Index, IndexMut};
use std::sync::Arc;
use std::sync::atomic::{AtomicPtr, Ordering};
use std::collections::HashMap;
use crate::fast_hash::HashMap;
use crate::u32_from_usize;
@ -147,7 +147,7 @@ impl StringInterner {
pub fn new() -> Self {
Self {
storage: Vec::new(),
lookup: HashMap::new(),
lookup: HashMap::default(),
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
//! Type aliases that swap the default RandomState (SipHash) for
//! foldhash's quality variant. Used in parser/scope-collector hot paths
//! where keys come from lexer tokens (i.e. attacker-controlled JS
//! source), so we keep formal HashDoS resistance while shedding
//! SipHash's per-byte cost.
pub type HashMap<K, V> = std::collections::HashMap<K, V, foldhash::quality::RandomState>;
pub type HashSet<T> = std::collections::HashSet<T, foldhash::quality::RandomState>;
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, foldhash::quality::RandomState>;

View file

@ -83,6 +83,7 @@ macro_rules! utf16 {
pub mod ast;
pub mod ast_dump;
pub mod bytecode;
pub mod fast_hash;
pub mod lexer;
pub mod parser;
pub mod scope_collector;

View file

@ -32,7 +32,7 @@
//! save and restore the full parser state including lexer position, current
//! token, error list, and all boolean flags.
use std::collections::{HashMap, HashSet};
use crate::fast_hash::{HashMap, HashSet};
use crate::ast::{
AstArena, BindingPattern, Expression, ExpressionKind, FunctionData, FunctionId, FunctionParameter, FunctionTable,
@ -323,7 +323,7 @@ impl<'a> Parser<'a> {
flags: ParserFlags::default(),
initiated_by_eval: false,
in_eval_function_context: false,
labels_in_scope: HashMap::new(),
labels_in_scope: HashMap::default(),
last_inner_label_is_iteration: false,
last_primary_was_parenthesized: false,
last_function_name: Utf16String::default(),
@ -340,11 +340,11 @@ impl<'a> Parser<'a> {
for_loop_declaration_is_var: false,
for_loop_declaration_is_pattern: false,
scope_collector: ScopeCollector::new(),
exported_names: HashSet::new(),
exported_names: HashSet::default(),
function_table: FunctionTable::new(),
arena: AstArena::new(),
function_context_stack: Vec::new(),
arrow_function_failed_positions: HashSet::new(),
arrow_function_failed_positions: HashSet::default(),
}
}
@ -880,7 +880,7 @@ impl<'a> Parser<'a> {
/// Check for duplicate parameter names in arrow functions.
/// Arrow functions always reject duplicates, regardless of strict mode.
pub(crate) fn check_arrow_duplicate_parameters(&mut self, parameter_info: &[ParamInfo]) {
let mut seen_names: HashSet<&[u16]> = HashSet::new();
let mut seen_names: HashSet<&[u16]> = HashSet::default();
for pi in parameter_info {
let name = &pi.name;
if name.is_empty() {
@ -903,7 +903,7 @@ impl<'a> Parser<'a> {
force_strict: bool,
_kind: FunctionKind,
) {
let mut seen_names: HashSet<&[u16]> = HashSet::new();
let mut seen_names: HashSet<&[u16]> = HashSet::default();
for pi in parameter_info {
let name = &pi.name;
if name.is_empty() {
@ -1124,7 +1124,7 @@ impl<'a> Parser<'a> {
use crate::ast::*;
// Collect all declared names at module level.
let mut declared_names: HashSet<Utf16String> = HashSet::new();
let mut declared_names: HashSet<Utf16String> = HashSet::default();
for child in children {
collect_module_declared_names(child, &mut declared_names, &self.arena);
}

View file

@ -6,7 +6,7 @@
//! Declaration parsing: variables, functions, classes, imports, exports.
use std::collections::{HashMap, HashSet};
use crate::fast_hash::{HashMap, HashSet};
use crate::ast::*;
use crate::lexer::ch;
@ -197,7 +197,7 @@ impl Parser<'_> {
}
if kind != DeclarationKind::Var {
let mut seen: HashSet<&[u16]> = HashSet::new();
let mut seen: HashSet<&[u16]> = HashSet::default();
for name in &name_strs {
if !seen.insert(name.as_slice()) {
self.syntax_error("Duplicate parameter names in bindings");
@ -682,9 +682,9 @@ impl Parser<'_> {
self.consume_token(TokenType::CurlyOpen);
let mut elements: Vec<Node<ClassElement>> = Vec::new();
let mut constructor: Option<Expression> = None;
let mut found_private_names: HashMap<Utf16String, (Option<ClassMethodKind>, bool)> = HashMap::new();
let mut found_private_names: HashMap<Utf16String, (Option<ClassMethodKind>, bool)> = HashMap::default();
self.referenced_private_names_stack.push(HashSet::new());
self.referenced_private_names_stack.push(HashSet::default());
let saved_class_has_super = self.class_has_super_class;
self.class_has_super_class = super_class.is_some();
@ -1379,7 +1379,7 @@ impl Parser<'_> {
// Validate duplicates after parsing so we can use borrowed name slices
// from `parameter_info` without cloning each entry into the HashSet.
let mut seen_parameter_names: HashSet<&[u16]> = HashSet::new();
let mut seen_parameter_names: HashSet<&[u16]> = HashSet::default();
let mut has_seen_non_simple = false;
for (start, end, parameter_is_non_simple) in parameter_info_ranges {
for info in &parameter_info[start..end] {

View file

@ -6,7 +6,7 @@
//! Statement parsing: if, for, while, switch, try, etc.
use std::collections::HashSet;
use crate::fast_hash::HashSet;
use crate::ast::*;
use crate::parser::{Associativity, ForbiddenTokens, PRECEDENCE_COMMA, Parser, Position};
@ -723,7 +723,7 @@ impl Parser<'_> {
// It is a Syntax Error if BoundNames of CatchParameter
// contains any duplicate elements.
{
let mut seen: HashSet<&[u16]> = HashSet::new();
let mut seen: HashSet<&[u16]> = HashSet::default();
for name in &names_to_check {
if !seen.insert(name.as_slice()) {
let name_str = String::from_utf16_lossy(name);

View file

@ -49,8 +49,7 @@
//! - `IdentifierGroup` — a set of identifier references with the same
//! name within one scope (multiple `foo` refs are grouped together)
use indexmap::IndexMap;
use std::collections::HashMap;
use crate::fast_hash::{HashMap, IndexMap};
use crate::ast::{
FunctionScopeData, IdentifierArena, IdentifierId, LocalBinding, LocalVarKind, LocalVariable, ScopeId, StringId,
@ -220,8 +219,8 @@ impl ScopeRecord {
scope_type,
scope_level,
scope_data,
variables: IndexMap::new(),
identifier_groups: IndexMap::new(),
variables: IndexMap::default(),
identifier_groups: IndexMap::default(),
functions_to_hoist: Vec::new(),
has_function_parameters: false,
parameter_names: Vec::new(),
@ -1272,8 +1271,8 @@ impl ScopeCollector {
// position matches. Keys are SharedUtf16String so each insert is a cheap
// Rc bump rather than a deep clone of the name.
let mut functions_to_initialize: Vec<crate::ast::FunctionToInit> = Vec::new();
let mut last_position: HashMap<StringId, usize> = HashMap::new();
let mut last_position_by_slice: HashMap<Utf16String, usize> = HashMap::new();
let mut last_position: HashMap<StringId, usize> = HashMap::default();
let mut last_position_by_slice: HashMap<Utf16String, usize> = HashMap::default();
{
let sd = &scopes[scope_id];
for (i, child) in sd.children.iter().enumerate() {