Skip to content

Commit 450d44e

Browse files
authored
perf: index EXT4 FileTree children by name to avoid O(n^2) unpack (#793)
- `FileTree.lookup` resolved each path component by linearly scanning the node's `children` array. This changes the node's child storage to an `OrderedDictionary<String, Ptr<FileTreeNode>>` (from swift-collections, which is already a package dependency) keyed by name, so `lookup` resolves each component in O(1) while iteration keeps the existing insertion order. - Little or no difference in unpack time for images with ~10k files, significant improvement for images with ~100k files or more.
1 parent 2563ed5 commit 450d44e

6 files changed

Lines changed: 31 additions & 26 deletions

File tree

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ let package = Package(
106106
name: "ContainerizationEXT4",
107107
dependencies: [
108108
"ContainerizationArchive",
109+
.product(name: "OrderedCollections", package: "swift-collections"),
109110
.product(name: "SystemPackage", package: "swift-system"),
110111
"ContainerizationOS",
111112
],

Sources/ContainerizationEXT4/EXT4+FileTree.swift

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
import Foundation
18+
import OrderedCollections
1819
import SystemPackage
1920

2021
extension EXT4 {
2122
class FileTree {
2223
class FileTreeNode {
2324
let inode: InodeNumber
2425
let name: String
25-
var children: [Ptr<FileTreeNode>] = []
26+
// Children keyed by name for O(1) lookup, preserving insertion order.
27+
private(set) var childrenByName: OrderedDictionary<String, Ptr<FileTreeNode>> = [:]
28+
var children: OrderedDictionary<String, Ptr<FileTreeNode>>.Values {
29+
childrenByName.values
30+
}
2631
var blocks: (start: UInt32, end: UInt32)?
2732
var additionalBlocks: [(start: UInt32, end: UInt32)]?
2833
var link: InodeNumber?
@@ -39,16 +44,17 @@ extension EXT4 {
3944
) {
4045
self.inode = inode
4146
self.name = name
42-
self.children = children
4347
self.blocks = blocks
4448
self.additionalBlocks = additionalBlocks
4549
self.link = link
4650
self.parent = parent
51+
for child in children {
52+
self.addChild(child)
53+
}
4754
}
4855

4956
deinit {
50-
self.children.removeAll()
51-
self.children = []
57+
self.childrenByName.removeAll()
5258
self.blocks = nil
5359
self.additionalBlocks = nil
5460
self.link = nil
@@ -64,6 +70,14 @@ extension EXT4 {
6470
let path = components.reversed().joined(separator: "/")
6571
return FilePath(path).lexicallyNormalized()
6672
}
73+
74+
func addChild(_ child: Ptr<FileTreeNode>) {
75+
childrenByName[child.pointee.name] = child
76+
}
77+
78+
func removeChild(named name: String) {
79+
childrenByName.removeValue(forKey: name)
80+
}
6781
}
6882

6983
var root: Ptr<FileTreeNode>
@@ -82,18 +96,10 @@ extension EXT4 {
8296
return node
8397
}
8498
for component in components {
85-
var found = false
86-
for childPtr in node.pointee.children {
87-
let child = childPtr.pointee
88-
if child.name == component {
89-
node = childPtr
90-
found = true
91-
break
92-
}
93-
}
94-
guard found else {
99+
guard let childPtr = node.pointee.childrenByName[component] else {
95100
return nil
96101
}
102+
node = childPtr
97103
}
98104
return node
99105
}

Sources/ContainerizationEXT4/EXT4+Formatter.swift

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ extension EXT4 {
186186
blocks: nil,
187187
link: targetNode.inode
188188
))
189-
parentTreeNode.children.append(linkTreeNodePtr)
189+
parentTreeNode.addChild(linkTreeNodePtr)
190190
parentTreeNodePtr.pointee = parentTreeNode
191191
}
192192

@@ -254,9 +254,7 @@ extension EXT4 {
254254
}
255255
}
256256
parentInodePtr.pointee = parentInode
257-
parentNode.children.removeAll { childPtr in
258-
childPtr.pointee.name == pathComponent
259-
}
257+
parentNode.removeChild(named: pathComponent)
260258
parentNodePtr.pointee = parentNode
261259

262260
if let hardlink = pathNode.link {
@@ -412,7 +410,7 @@ extension EXT4 {
412410
children: [],
413411
blocks: (startBlock, endBlock)
414412
))
415-
parentTreeNode.children.append(childTreeNodePtr)
413+
parentTreeNode.addChild(childTreeNodePtr)
416414
parentTreeNodePtr.pointee = parentTreeNode
417415
}
418416
childInode.mode = mode

Sources/ContainerizationEXT4/EXT4+Reader.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ extension EXT4 {
102102
itemTreeNode.blocks = blocks.first
103103
}
104104
let itemTreeNodePtr = Ptr(itemTreeNode)
105-
root.children.append(itemTreeNodePtr)
105+
root.addChild(itemTreeNodePtr)
106106
itemPtr.pointee = root
107107
let itemInode = try self.getInode(number: itemInodeNum)
108108
if itemInode.mode.isDir() {

Sources/ContainerizationEXT4/EXT4Reader+Export.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension EXT4.EXT4Reader {
2424
format: .paxRestricted, filter: .none, options: [Options.xattrformat(.schily)])
2525
let writer = try ArchiveWriter(configuration: config)
2626
try writer.open(file: archive.url)
27-
var items = self.tree.root.pointee.children
27+
var items = Array(self.tree.root.pointee.children)
2828
let hardlinkedInodes = Set(self.hardlinks.values)
2929
var hardlinkTargets: [EXT4.InodeNumber: FilePath] = [:]
3030

Tests/ContainerizationEXT4Tests/TestEXT4Reader+IO.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -617,10 +617,10 @@ struct EXT4PathIOTests {
617617
let tree = EXT4.FileTree(EXT4.RootInode, "/")
618618

619619
let dirPtr = EXT4.Ptr(EXT4.FileTree.FileTreeNode(inode: 3, name: "dir", parent: tree.root))
620-
tree.root.pointee.children.append(dirPtr)
620+
tree.root.pointee.addChild(dirPtr)
621621

622622
let filePtr = EXT4.Ptr(EXT4.FileTree.FileTreeNode(inode: 4, name: "file", parent: dirPtr))
623-
dirPtr.pointee.children.append(filePtr)
623+
dirPtr.pointee.addChild(filePtr)
624624

625625
#expect(dirPtr.pointee.path == FilePath("/dir"))
626626
#expect(filePtr.pointee.path == FilePath("/dir/file"))
@@ -631,10 +631,10 @@ struct EXT4PathIOTests {
631631
let tree = EXT4.FileTree(EXT4.RootInode, ".")
632632

633633
let dirPtr = EXT4.Ptr(EXT4.FileTree.FileTreeNode(inode: 3, name: "dir", parent: tree.root))
634-
tree.root.pointee.children.append(dirPtr)
634+
tree.root.pointee.addChild(dirPtr)
635635

636636
let filePtr = EXT4.Ptr(EXT4.FileTree.FileTreeNode(inode: 4, name: "file", parent: dirPtr))
637-
dirPtr.pointee.children.append(filePtr)
637+
dirPtr.pointee.addChild(filePtr)
638638

639639
#expect(dirPtr.pointee.path == FilePath("dir"))
640640
#expect(filePtr.pointee.path == FilePath("dir/file"))
@@ -645,7 +645,7 @@ struct EXT4PathIOTests {
645645
let tree = EXT4.FileTree(EXT4.RootInode, "dir")
646646

647647
let filePtr = EXT4.Ptr(EXT4.FileTree.FileTreeNode(inode: 3, name: "file", parent: tree.root))
648-
tree.root.pointee.children.append(filePtr)
648+
tree.root.pointee.addChild(filePtr)
649649

650650
#expect(filePtr.pointee.path == FilePath("dir/file"))
651651
}

0 commit comments

Comments
 (0)