Skip to content

Commit

Permalink
fix: order files in asar for optimized differential updates (#8128)
Browse files Browse the repository at this point in the history
ASAR file begins with a header that list all files and an offset to each
file in the rest of the file. When a file placed early in ASAR changes
its length - it means that all subsequent file declarations in the
header will have their offsets updated. While harmless by itself, this
negatively affects the incremental download size as more of the
installer binary is different from what it used to be.

In this change we order files in asar such that:

- Dependencies/node_modules come first (they change least often)
- Main app files come last (they change more frequently)

Additionally, files in asar are now ordered alphabetically within each
fileset to guarantee stable output.

All of above results in 2x improvement of incremental download size.
  • Loading branch information
indutny-signal committed Mar 12, 2024
1 parent 445911a commit 555dc90
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 114 deletions.
5 changes: 5 additions & 0 deletions .changeset/fair-pants-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"app-builder-lib": patch
---

fix: order files within asar for smaller incremental updates
65 changes: 63 additions & 2 deletions packages/app-builder-lib/src/asar/asarUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,18 @@ export class AsarPackager {
}
await mkdir(path.dirname(this.outFile), { recursive: true })
const unpackedFileIndexMap = new Map<ResolvedFileSet, Set<number>>()
for (const fileSet of fileSets) {
const orderedFileSets = [
// Write dependencies first to minimize offset changes to asar header
...fileSets.slice(1),

// Finish with the app files that change most often
fileSets[0],
].map(orderFileSet)

for (const fileSet of orderedFileSets) {
unpackedFileIndexMap.set(fileSet, await this.createPackageFromFiles(fileSet, packager.info))
}
await this.writeAsarFile(fileSets, unpackedFileIndexMap)
await this.writeAsarFile(orderedFileSets, unpackedFileIndexMap)
}

private async createPackageFromFiles(fileSet: ResolvedFileSet, packager: Packager) {
Expand Down Expand Up @@ -270,3 +278,56 @@ function copyFileOrData(fileCopier: FileCopier, data: string | Buffer | undefine
return writeFile(destination, data)
}
}

function orderFileSet(fileSet: ResolvedFileSet): ResolvedFileSet {
const sortedFileEntries = Array.from(fileSet.files.entries())

sortedFileEntries.sort(([, a], [, b]) => {
if (a === b) {
return 0
}

// Place addons last because their signature change
const isAAddon = a.endsWith(".node")
const isBAddon = b.endsWith(".node")
if (isAAddon && !isBAddon) {
return 1
}
if (isBAddon && !isAAddon) {
return -1
}

// Otherwise order by name
return a < b ? -1 : 1
})

let transformedFiles: Map<number, string | Buffer> | undefined
if (fileSet.transformedFiles) {
transformedFiles = new Map()

const indexMap = new Map<number, number>()
for (const [newIndex, [oldIndex]] of sortedFileEntries.entries()) {
indexMap.set(oldIndex, newIndex)
}

for (const [oldIndex, value] of fileSet.transformedFiles) {
const newIndex = indexMap.get(oldIndex)
if (newIndex === undefined) {
const file = fileSet.files[oldIndex]
throw new Error(`Internal error: ${file} was lost while ordering asar`)
}

transformedFiles.set(newIndex, value)
}
}

const { src, destination, metadata } = fileSet

return {
src,
destination,
metadata,
files: sortedFileEntries.map(([, file]) => file),
transformedFiles,
}
}
Loading

0 comments on commit 555dc90

Please sign in to comment.