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:
parent
17d3a285a7
commit
d9dd412440
9 changed files with 39 additions and 22 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -375,6 +375,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bytecode_def",
|
||||
"cbindgen",
|
||||
"foldhash",
|
||||
"indexmap",
|
||||
"libunicode_rust",
|
||||
"num-bigint",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
15
Libraries/LibJS/Rust/src/fast_hash.rs
Normal file
15
Libraries/LibJS/Rust/src/fast_hash.rs
Normal 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>;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ¶meter_info[start..end] {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue