ladybird/Libraries/LibWeb/HTML/Parser/Entities.h
Ryan Liptak 6da1dfa8f2 LibWeb/HTML: Improve data structure of named character reference data
Introduces a few ad-hoc modifications to the DAFSA aimed to increase
performance while keeping the data size small.

- The 'first layer' of nodes is extracted out and replaced with a lookup
  table. This turns the search for the first character from O(n) to O
  (1), and doesn't increase the data size because all first characters
  in the set of named character references have the
  values 'a'-'z'/'A'-'Z', so a lookup array of exactly 52 elements can
  be used. The lookup table stores the cumulative "number" fields that
  would be calculated by a linear scan that matches a given node, thus
  allowing the unique index to be built-up as normal with a O(1) search
  instead of a linear scan.
- The 'second layer' of nodes is also extracted out and searches of the
  second layer are done using a bit field of 52 bits (the set bits of
  the bit field depend on the first character's value), where each set
  bit corresponds to one of 'a'-'z'/'A'-'Z' (similar to the first
  layer, the second layer can only contain ASCII alphabetic
  characters). The bit field is then re-used (along with an offset) to
  get the index into the array of second layer nodes. This technique
  ultimately allows for storing the minimum number of nodes in the
  second layer, and therefore only increasing the size of the data by
  the size of the 'first to second layer link' info which is 52 * 8 =
  416 bytes.
- After the second layer, the rest of the data is stored using a
  mostly-normal DAFSA, but there are still a few differences:
   - The "number" field is cumulative, in the same way that the
     first/second layer store a cumulative "number" field. This cuts
     down slightly on the amount of work done during the search of a
     list of children, and we can get away with it because the
     cumulative "number" fields of the remaining nodes in the DAFSA
     (after the first and second layer nodes were extracted out) happens
     to require few enough bits that we can store the cumulative version
     while staying under our 32-bit budget.
   - Instead of storing a 'last sibling' flag to denote the end of a
     list of children, the length of each node's list of children is
     stored. Again, this is mostly done just because there are enough
     bits available to do so while keeping the DAFSA node within 32
     bits.
   - Note: Together, these modifications open up the possibility of
     using a binary search instead of a linear search over the
     children, but due to the consistently small lengths of the lists
     of children in the remaining DAFSA, a linear search actually seems
     to be the better option.

The new data size is 24,724 bytes, up from 24,412 bytes (+312, -104 from
the 52 first layer nodes going from 4-bytes to 2-bytes, and +416 from
the addition of the 'first to second layer link' data).

In terms of raw matching speed (outside the context of the tokenizer),
this provides about a 1.72x speedup.

In very named-character-reference-heavy tokenizer benchmarks, this
provides about a 1.05x speedup (the effect of named character reference
matching speed is diluted when benchmarking the tokenizer).

Additionally, fixes the size of the named character reference data when
targeting Windows.
2025-07-14 09:43:08 +02:00

60 lines
1.9 KiB
C++

/*
* Copyright (c) 2020, Andreas Kling <andreas@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/Span.h>
#include <LibWeb/HTML/Parser/NamedCharacterReferences.h>
namespace Web::HTML {
class NamedCharacterReferenceMatcher {
public:
NamedCharacterReferenceMatcher() = default;
// If `c` is the codepoint of a child of the current `node_index`, the `node_index`
// is updated to that child and the function returns `true`.
// Otherwise, the `node_index` is unchanged and the function returns false.
bool try_consume_code_point(u32 c)
{
if (c > 0x7F)
return false;
return try_consume_ascii_char(static_cast<u8>(c));
}
// If `c` is the character of a child of the current `node_index`, the `node_index`
// is updated to that child and the function returns `true`.
// Otherwise, the `node_index` is unchanged and the function returns false.
bool try_consume_ascii_char(u8 c);
// Returns the code points associated with the last match, if any.
Optional<NamedCharacterReferenceCodepoints> code_points() const { return named_character_reference_codepoints_from_unique_index(m_last_matched_unique_index); }
bool last_match_ends_with_semicolon() const { return m_ends_with_semicolon; }
u8 overconsumed_code_points() const { return m_overconsumed_code_points; }
private:
enum class SearchStateTag : u8 {
Init,
FirstToSecondLayer,
DafsaChildren,
};
union SearchState {
NamedCharacterReferenceFirstToSecondLayerLink first_to_second_layer;
ReadonlySpan<NamedCharacterReferenceNode> dafsa_children;
};
SearchStateTag m_search_state_tag { SearchStateTag::Init };
SearchState m_search_state { { 0, 0 } };
u16 m_last_matched_unique_index { 0 };
u16 m_pending_unique_index { 0 };
u8 m_overconsumed_code_points { 0 };
bool m_ends_with_semicolon { false };
};
}