From 4b0aef5439601df9a519048de2afe6ded93a1a04 Mon Sep 17 00:00:00 2001 From: Alex Ehlke Date: Fri, 28 Feb 2025 18:28:18 -0500 Subject: [PATCH] optimizations --- Sources/Attributes.swift | 46 +++++++++++++++++++++---------------- Sources/Node.swift | 24 +++++++++++++------ Sources/StringBuilder.swift | 16 +++++++++---- Sources/Token.swift | 1 + Sources/Tokeniser.swift | 7 ++++-- Sources/UTF8Arrays.swift | 2 ++ 6 files changed, 62 insertions(+), 34 deletions(-) diff --git a/Sources/Attributes.swift b/Sources/Attributes.swift index 5b70f3ba..80d8f6ae 100644 --- a/Sources/Attributes.swift +++ b/Sources/Attributes.swift @@ -27,10 +27,21 @@ open class Attributes: NSCopying { // Stored by lowercased key, but key case is checked against the copy inside // the Attribute on retrieval. - var attributes: [Attribute] = [] + lazy var attributes: [Attribute] = [] + internal var lowercasedKeysCache: [[UInt8]]? = nil public init() {} - + + @usableFromInline + internal func updateLowercasedKeysCache() { + lowercasedKeysCache = attributes.map { $0.getKeyUTF8().map { Self.asciiLowercase($0) } } + } + + @usableFromInline + internal func invalidateLowercasedKeysCache() { + lowercasedKeysCache = nil + } + /** Get an attribute value by key. @param key the (case-sensitive) attribute key @@ -103,8 +114,9 @@ open class Attributes: NSCopying { } else { attributes.append(attribute) } + invalidateLowercasedKeysCache() } - + /** Remove an attribute by key. Case sensitive. @param key attribute key to remove @@ -116,7 +128,9 @@ open class Attributes: NSCopying { open func remove(key: [UInt8]) throws { try Validate.notEmpty(string: key) if let ix = attributes.firstIndex(where: { $0.getKeyUTF8() == key }) { - attributes.remove(at: ix) } + attributes.remove(at: ix) + invalidateLowercasedKeysCache() + } } /** @@ -127,6 +141,7 @@ open class Attributes: NSCopying { try Validate.notEmpty(string: key) if let ix = attributes.firstIndex(where: { $0.getKeyUTF8().caseInsensitiveCompare(key) == .orderedSame}) { attributes.remove(at: ix) + invalidateLowercasedKeysCache() } } @@ -148,32 +163,23 @@ open class Attributes: NSCopying { @param key key to check for @return true if key exists, false otherwise */ + @inlinable open func hasKeyIgnoreCase(key: String) -> Bool { return hasKeyIgnoreCase(key: key.utf8Array) } @inline(__always) - private func asciiLowercase(_ byte: UInt8) -> UInt8 { + internal static func asciiLowercase(_ byte: UInt8) -> UInt8 { return (byte >= 65 && byte <= 90) ? (byte + 32) : byte } open func hasKeyIgnoreCase(key: T) -> Bool where T.Element == UInt8 { - let keyCount = key.count - for attr in attributes { - let attrKey = attr.getKeyUTF8() - if attrKey.count != keyCount { continue } - var attrIter = attrKey.makeIterator() - var keyIter = key.makeIterator() - var equal = true - while let a = attrIter.next(), let b = keyIter.next() { - if asciiLowercase(a) != asciiLowercase(b) { - equal = false - break - } - } - if equal { return true } + try? Validate.notEmpty(string: Array(key)) + if lowercasedKeysCache == nil { + updateLowercasedKeysCache() } - return false + let lowerQuery = key.lazy.map { Self.asciiLowercase($0) } + return lowercasedKeysCache!.contains { $0.elementsEqual(lowerQuery) } } /** diff --git a/Sources/Node.swift b/Sources/Node.swift index 9276077f..56e27014 100644 --- a/Sources/Node.swift +++ b/Sources/Node.swift @@ -52,6 +52,7 @@ open class Node: Equatable, Hashable { public private(set) var siblingIndex: Int = 0 private static let abs = "abs:".utf8Array + private static let absCount = abs.count fileprivate static let empty = "".utf8Array private static let EMPTY_NODES: Array = Array() @@ -159,11 +160,20 @@ open class Node: Equatable, Hashable { * @return true if the attribute exists, false if not. */ open func hasAttr(_ attributeKey: String) -> Bool { - guard let attributes = attributes else { - return false - } - if attributeKey.utf8.starts(with: Node.abs) { - let key = ArraySlice(attributeKey.utf8.dropFirst(Node.abs.count)) + return hasAttr(attributeKey.utf8Array) + } + + /** + * Test if this element has an attribute. Case insensitive + * @param attributeKey The attribute key to check. + * @return true if the attribute exists, false if not. + */ + open func hasAttr(_ attributeKey: [UInt8]) -> Bool { + guard let attributes = attributes else { + return false + } + if attributeKey.starts(with: Node.abs) { + let key = ArraySlice(attributeKey.dropFirst(Node.absCount)) do { let abs = try absUrl(key) if (attributes.hasKeyIgnoreCase(key: key) && !abs.isEmpty) { @@ -172,11 +182,11 @@ open class Node: Equatable, Hashable { } catch { return false } - + } return attributes.hasKeyIgnoreCase(key: attributeKey) } - + /** * Remove an attribute from this element. * @param attributeKey The attribute to remove. diff --git a/Sources/StringBuilder.swift b/Sources/StringBuilder.swift index ebe8021f..2cec8378 100755 --- a/Sources/StringBuilder.swift +++ b/Sources/StringBuilder.swift @@ -14,10 +14,12 @@ open class StringBuilder { if !string.isEmpty { buffer.append(contentsOf: string.utf8) } + buffer.reserveCapacity(string.utf8.count ?? 128) } public init(_ size: Int) { - self.buffer = Array() + buffer = Array() + buffer.reserveCapacity(size) } /** @@ -47,24 +49,24 @@ open class StringBuilder { :return: reference to this StringBuilder instance */ - @inlinable + @inline(__always) @discardableResult open func append(_ string: String) -> StringBuilder { buffer.append(contentsOf: string.utf8) return self } - @inlinable + @inline(__always) open func append(_ chr: Character) { append(String(chr)) } - @inlinable + @inline(__always) open func appendCodePoints(_ chr: [Character]) { append(String(chr)) } - @inlinable + @inline(__always) open func appendCodePoint(_ ch: Int) { appendCodePoint(UnicodeScalar(ch)!) } @@ -181,6 +183,7 @@ open class StringBuilder { :return: reference to this StringBuilder instance */ @discardableResult + @inlinable open func clear() -> StringBuilder { buffer.removeAll(keepingCapacity: true) return self @@ -193,6 +196,7 @@ open class StringBuilder { :param: lhs StringBuilder :param: rhs String */ +@inlinable public func += (lhs: StringBuilder, rhs: String) { lhs.append(rhs) } @@ -203,6 +207,7 @@ public func += (lhs: StringBuilder, rhs: String) { :param: lhs Printable :param: rhs String */ +@inlinable public func += (lhs: StringBuilder, rhs: T) { lhs.append(rhs.description) } @@ -215,6 +220,7 @@ public func += (lhs: StringBuilder, rhs: T) { :result StringBuilder */ +@inlinable public func +(lhs: StringBuilder, rhs: StringBuilder) -> StringBuilder { return StringBuilder(string: lhs.toString() + rhs.toString()) } diff --git a/Sources/Token.swift b/Sources/Token.swift index 3825750b..f1a138a8 100644 --- a/Sources/Token.swift +++ b/Sources/Token.swift @@ -27,6 +27,7 @@ open class Token { preconditionFailure("This method must be overridden") } + @inlinable static func reset(_ sb: StringBuilder) { sb.clear() } diff --git a/Sources/Tokeniser.swift b/Sources/Tokeniser.swift index e6fc7f68..48cd087a 100644 --- a/Sources/Tokeniser.swift +++ b/Sources/Tokeniser.swift @@ -207,11 +207,14 @@ final class Tokeniser { } @discardableResult + @inlinable func createTagPending(_ start: Bool) -> Token.Tag { - tagPending = start ? startPending.reset() : endPending.reset() - return tagPending + let token: Token.Tag = start ? Token.StartTag() : Token.EndTag() + tagPending = token + return token } + @inlinable func emitTagPending() throws { try tagPending.finaliseTag() try emit(tagPending) diff --git a/Sources/UTF8Arrays.swift b/Sources/UTF8Arrays.swift index 525d8bce..5b4e9796 100644 --- a/Sources/UTF8Arrays.swift +++ b/Sources/UTF8Arrays.swift @@ -80,6 +80,7 @@ public enum UTF8Arrays { public static let br = "br".utf8Array public static let frameset = "frameset".utf8Array public static let blobColon = "blob:".utf8Array + public static let true_ = "true".utf8Array } public enum UTF8ArraySlices { @@ -162,4 +163,5 @@ public enum UTF8ArraySlices { public static let br = UTF8Arrays.br[...] public static let frameset = UTF8Arrays.frameset[...] public static let blobColon = UTF8Arrays.blobColon[...] + public static let true_ = UTF8Arrays.true_[...] }