Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
4f46dd1
perf: add Install-NerdFont performance measurement script
MariusStorhaug May 17, 2026
e63bfc1
perf: suppress Invoke-WebRequest progress bar during font downloads
MariusStorhaug May 17, 2026
acb9884
perf: dedup font set with List+HashSet, drop O(n^2) array growth
MariusStorhaug May 17, 2026
ddd7142
perf: skip download when font is already installed (closes #73)
MariusStorhaug May 17, 2026
414f423
fix: handle empty Get-Font result in skip-installed check
MariusStorhaug May 17, 2026
c3eddbf
perf: extract font archives via ZipFile API (closes #72)
MariusStorhaug May 17, 2026
955ea2c
perf: cache downloaded font archives between invocations (closes #76)
MariusStorhaug May 17, 2026
3f208ea
perf: add -Variant filter to install only desired font variants (clos…
MariusStorhaug May 17, 2026
6df13c6
style: split long Variant switch arms for PSUseConsistentWhitespace/P…
MariusStorhaug May 17, 2026
7a054ea
test: add -Variant Mono case to lift code coverage above threshold
MariusStorhaug May 17, 2026
ddd8014
perf: parallelize font downloads/extract (closes #71)
MariusStorhaug May 17, 2026
d041a0b
perf: parallel HTTP downloads via HttpClient tasks (closes #71)
MariusStorhaug May 17, 2026
1572b9a
perf: bound parallel HTTP downloads to 8 concurrent (closes #71)
MariusStorhaug May 17, 2026
ed569aa
Document variant installs and cache behavior
MariusStorhaug May 17, 2026
62233a5
Handle partial download failures in Install-NerdFont
MariusStorhaug May 17, 2026
4ea2bdc
Fix Install-NerdFont indentation for CI
MariusStorhaug May 17, 2026
5d90644
Normalize queued download object indentation
MariusStorhaug May 17, 2026
cadd8cf
Add coverage for installed-font skip path
MariusStorhaug May 17, 2026
5d052b7
Use processor count for default parallelism
MariusStorhaug May 17, 2026
a7223f4
Remove perf-results.jsonl from .gitignore and add the file with perfo…
MariusStorhaug May 17, 2026
a63442d
Increase Install-NerdFont coverage with Standard variant test
MariusStorhaug May 17, 2026
d8d51c2
Fix formatting of Write-Host output in Measure-InstallPerformance.ps1
MariusStorhaug May 17, 2026
34de022
Stabilize all-font test and fix recursive temp cleanup
MariusStorhaug May 17, 2026
5183ed2
Address PR #77 review threads: cache resilience, variant dedupe, docs…
MariusStorhaug May 18, 2026
1a4120e
Fix Sort-Object property expressions not being passed to the command;…
MariusStorhaug May 18, 2026
75feaf4
Run duplicate-file removal for all variant selections, not just non-All
MariusStorhaug May 18, 2026
634c91c
Fix prefix false-positive in skip check, cap download throttle at 8, …
MariusStorhaug May 18, 2026
2b35f57
Fix cache resilience and strengthen variant test assertions
MariusStorhaug May 18, 2026
ef4c99f
Add test for cache-read failure fallback to improve coverage
MariusStorhaug May 18, 2026
00d4a36
Fix CI test failures: isolate variant tests from Fonts module, fix ca…
MariusStorhaug May 19, 2026
f3f71bd
Defer cache directory creation until approved by ShouldProcess; fix p…
MariusStorhaug May 19, 2026
f8137f2
Clean up test-created cache directories so tests leave no persistent …
MariusStorhaug May 19, 2026
5262f49
Refactor tests to use InModuleScope for module-level coverage; add Pr…
MariusStorhaug May 19, 2026
2ae527f
Fix CI test failures and address cache write review threads
MariusStorhaug May 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ To download the font from the NerdFonts repository and install it on the system,
Install-NerdFont -Name 'FiraCode' -Scope AllUsers #Tab completion works on Scope too
```

To install only a specific variant from the archive, use the `-Variant` parameter. `Mono` is useful for terminal and editor setups where you only want the monospace family.

```powershell
Install-NerdFont -Name 'FiraCode' -Variant Mono
```

### Install all NerdFonts

To install all NerdFonts on the system you can use the following command.
Expand All @@ -54,6 +60,12 @@ This requires the shell to run in an elevated context (sudo or run as administra
Install-NerdFont -All -Scope AllUsers
```

You can combine `-All` with `-Variant` to limit what gets installed from each archive:

```powershell
Install-NerdFont -All -Variant Mono
```

### Check if a NerdFont is installed

The [Fonts](https://psmodule.io/Fonts) module is installed automatically as a dependency and provides the
Expand All @@ -73,6 +85,23 @@ Get-Font -Name 'FiraCode*' -Scope AllUsers

If the command returns results, the font is installed. If it returns nothing, the font is not installed in that scope.

When you run `Install-NerdFont` again without `-Force`, fonts that are already installed in the requested scope are skipped. Downloaded archives are also cached per Nerd Fonts release so retries and repeated installs do not need to fetch the same ZIP again.

Comment thread
MariusStorhaug marked this conversation as resolved.
Cache locations:

- Windows: `%LOCALAPPDATA%/PSModule/NerdFonts/cache`
- macOS and Linux: `$HOME/.cache/PSModule/NerdFonts`

You can inspect the active cache path in PowerShell with:

```powershell
if ($IsWindows) {
Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'PSModule/NerdFonts/cache'
} else {
Join-Path $HOME '.cache/PSModule/NerdFonts'
}
```

### Update an installed NerdFont

Individual font files do not embed a NerdFonts release version, so there is no direct way to check whether an installed
Expand All @@ -89,7 +118,7 @@ Install-NerdFont -Name 'FiraCode' -Force -Scope AllUsers
```

This re-downloads and installs the font version bundled with your installed NerdFonts module, overwriting any existing
files. To pick up newer font releases, update the NerdFonts module first (`Update-PSResource -Name NerdFonts` if you
files. `-Force` also bypasses the local archive cache so the font ZIP is fetched again before reinstalling. To pick up newer font releases, update the NerdFonts module first (`Update-PSResource -Name NerdFonts` if you
installed via PSResourceGet, or `Update-Module -Name NerdFonts` if you installed via PowerShellGet).

### Uninstall a NerdFont
Expand Down
152 changes: 152 additions & 0 deletions scripts/Measure-InstallPerformance.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<#
.SYNOPSIS
Measures Install-NerdFont performance across known scenarios.

.DESCRIPTION
Runs a set of timed scenarios against the currently loaded NerdFonts module
and emits a structured result object per scenario. Each scenario uninstalls
the fonts it will measure before timing, so measurements are comparable
across iterations.

.EXAMPLE
./Measure-InstallPerformance.ps1 -Iteration 'baseline' -Subset 'Hack','FiraCode','JetBrainsMono'

.NOTES
Per-iteration result JSON is appended to scripts/perf-results.jsonl
so a full report can be produced at the end of the improvement cycle.
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidUsingWriteHost', '',
Justification = 'Console output for an interactive perf script.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSReviewUnusedParameter', 'ResultsPath',
Justification = 'Used inside Measure-Scenario via closure on the enclosing scope.'
)]
[CmdletBinding()]
param(
# Free-form label for the iteration (module version, commit short SHA, etc).
[Parameter(Mandatory)]
[string] $Iteration,

# Named fonts used for the small subset scenarios. Should be small/medium
# archives to keep iteration time bounded.
[Parameter()]
[string[]] $Subset = @('Hack', 'FiraCode', 'JetBrainsMono'),

# When set, also runs a full Install-NerdFont -All measurement. Slow.
[Parameter()]
[switch] $IncludeAll,

# File where per-scenario JSON lines are appended.
[Parameter()]
[string] $ResultsPath = (Join-Path $PSScriptRoot 'perf-results.jsonl')
)

$ErrorActionPreference = 'Stop'

function Invoke-Uninstall {
<#
.SYNOPSIS
Removes matching Nerd Font families for the current user.
#>
param([string[]]$Names)
foreach ($n in $Names) {
# Nerd Fonts archives expand to multiple family names that all start
# with the archive's base name (e.g. "Hack Nerd Font", "Hack Nerd Font Mono").
$families = Get-Font -Scope CurrentUser | Where-Object { $_.Name -like "$n Nerd Font*" }
foreach ($f in $families) {
try {
Uninstall-Font -Name $f.Name -Scope CurrentUser -ErrorAction Stop
} catch {
Write-Verbose "Uninstall failed for $($f.Name): $_"
}
}
}
}

function Invoke-UninstallAll {
<#
.SYNOPSIS
Removes all Nerd Fonts known to the current module.
#>
$names = (Get-NerdFont).Name
Invoke-Uninstall -Names $names
}

function Measure-Scenario {
<#
.SYNOPSIS
Runs one setup/action performance scenario and records the result.
#>
param(
[string]$Name,
[scriptblock]$Setup,
[scriptblock]$Action
)
Write-Host "[$Iteration] Setup : $Name" -ForegroundColor DarkGray
& $Setup | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
Write-Host "[$Iteration] Measure : $Name" -ForegroundColor Cyan
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
& $Action | Out-Null
$err = $null
} catch {
$err = $_.ToString()
}
$sw.Stop()
$result = [pscustomobject]@{
Iteration = $Iteration
Scenario = $Name
DurationMs = [int]$sw.Elapsed.TotalMilliseconds
DurationS = [math]::Round($sw.Elapsed.TotalSeconds, 2)
Timestamp = (Get-Date).ToString('o')
Error = $err
Module = (Get-Module NerdFonts).Version.ToString()
}
Write-Host ("[$Iteration] Result : {0} -> {1}s" -f $Name, $result.DurationS) -ForegroundColor Green
$result | ConvertTo-Json -Compress | Add-Content -Path $ResultsPath
return $result
}

$results = [System.Collections.Generic.List[object]]::new()

# --- Scenario 1: single small/medium font ---
$single = @{
Name = 'Single-Hack'
Setup = { Invoke-Uninstall -Names 'Hack' }
Action = { Install-NerdFont -Name 'Hack' -Scope CurrentUser -Force }
}
$results.Add((Measure-Scenario @single))

# --- Scenario 2: subset of named fonts ---
$subsetArgs = @{
Name = "Subset-$($Subset -join '+')"
Setup = { Invoke-Uninstall -Names $Subset }
Action = { Install-NerdFont -Name $Subset -Scope CurrentUser -Force }
}
$results.Add((Measure-Scenario @subsetArgs))

# --- Scenario 3: re-install when already present (no-op path) ---
$noop = @{
Name = 'Subset-AlreadyInstalled'
Setup = { }
Comment thread
MariusStorhaug marked this conversation as resolved.
Action = { Install-NerdFont -Name $Subset -Scope CurrentUser }
}
$results.Add((Measure-Scenario @noop))

# --- Scenario 4: full -All (only when explicitly requested) ---
if ($IncludeAll) {
$allArgs = @{
Name = 'All'
Setup = { Invoke-UninstallAll }
Action = { Install-NerdFont -All -Scope CurrentUser -Force }
}
$results.Add((Measure-Scenario @allArgs))
}

Write-Host ''
Write-Host "Summary for iteration '$Iteration':" -ForegroundColor Yellow
$results | Format-Table Iteration, Scenario, DurationS, Module -AutoSize
32 changes: 32 additions & 0 deletions scripts/perf-results.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{"Iteration":"baseline-1.0.32","Scenario":"Single-Hack","DurationMs":1300,"DurationS":1.3,"Timestamp":"2026-05-17T12:49:25.3421303+02:00","Error":null,"Module":"1.0.32"}
{"Iteration":"baseline-1.0.32","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":6195,"DurationS":6.2,"Timestamp":"2026-05-17T12:49:31.7706509+02:00","Error":null,"Module":"1.0.32"}
{"Iteration":"baseline-1.0.32","Scenario":"Subset-AlreadyInstalled","DurationMs":2808,"DurationS":2.81,"Timestamp":"2026-05-17T12:49:34.6122341+02:00","Error":null,"Module":"1.0.32"}
{"Iteration":"1.0.33-pre001-progressbar","Scenario":"Single-Hack","DurationMs":675,"DurationS":0.67,"Timestamp":"2026-05-17T12:59:23.3818901+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre001-progressbar","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3583,"DurationS":3.58,"Timestamp":"2026-05-17T12:59:28.5904267+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre001-progressbar","Scenario":"Subset-AlreadyInstalled","DurationMs":2867,"DurationS":2.87,"Timestamp":"2026-05-17T12:59:31.4997320+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre002-dedup","Scenario":"Single-Hack","DurationMs":571,"DurationS":0.57,"Timestamp":"2026-05-17T13:09:00.7799184+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre002-dedup","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3519,"DurationS":3.52,"Timestamp":"2026-05-17T13:09:05.9546030+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre002-dedup","Scenario":"Subset-AlreadyInstalled","DurationMs":2835,"DurationS":2.83,"Timestamp":"2026-05-17T13:09:08.8327714+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre003-skipinstalled","Scenario":"Single-Hack","DurationMs":703,"DurationS":0.7,"Timestamp":"2026-05-17T13:22:50.2667008+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre003-skipinstalled","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3559,"DurationS":3.56,"Timestamp":"2026-05-17T13:22:56.1123334+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre003-skipinstalled","Scenario":"Subset-AlreadyInstalled","DurationMs":22,"DurationS":0.02,"Timestamp":"2026-05-17T13:22:56.1734595+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre004-zipfile","Scenario":"Single-Hack","DurationMs":598,"DurationS":0.6,"Timestamp":"2026-05-17T13:31:16.9883423+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre004-zipfile","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3440,"DurationS":3.44,"Timestamp":"2026-05-17T13:31:22.7322456+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre004-zipfile","Scenario":"Subset-AlreadyInstalled","DurationMs":16,"DurationS":0.02,"Timestamp":"2026-05-17T13:31:22.7860048+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-cold","Scenario":"Single-Hack","DurationMs":446,"DurationS":0.45,"Timestamp":"2026-05-17T13:40:07.8376702+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-cold","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3656,"DurationS":3.66,"Timestamp":"2026-05-17T13:40:13.8549945+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-cold","Scenario":"Subset-AlreadyInstalled","DurationMs":17,"DurationS":0.02,"Timestamp":"2026-05-17T13:40:13.9101232+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-warm","Scenario":"Single-Hack","DurationMs":403,"DurationS":0.4,"Timestamp":"2026-05-17T13:40:14.5276409+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-warm","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3193,"DurationS":3.19,"Timestamp":"2026-05-17T13:40:19.2900547+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre005-cache-warm","Scenario":"Subset-AlreadyInstalled","DurationMs":11,"DurationS":0.01,"Timestamp":"2026-05-17T13:40:19.3360365+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre006-variant-all","Scenario":"Single-Hack","DurationMs":1171,"DurationS":1.17,"Timestamp":"2026-05-17T14:05:52.0905786+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre006-variant-all","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":6096,"DurationS":6.1,"Timestamp":"2026-05-17T14:06:02.0498948+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre006-variant-all","Scenario":"Subset-AlreadyInstalled","DurationMs":35,"DurationS":0.04,"Timestamp":"2026-05-17T14:06:02.1490854+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre006-variant-mono","Scenario":"Subset-Mono","DurationMs":1620,"DurationS":1.62,"Timestamp":"2026-05-17T12:06:18.1677417Z","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel","Scenario":"Single-Hack","DurationMs":615,"DurationS":0.61,"Timestamp":"2026-05-17T14:32:21.0280298+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3695,"DurationS":3.7,"Timestamp":"2026-05-17T14:32:24.9507727+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel","Scenario":"Subset-AlreadyInstalled","DurationMs":16,"DurationS":0.02,"Timestamp":"2026-05-17T14:32:25.0140743+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel-all","Scenario":"Single-Hack","DurationMs":466,"DurationS":0.47,"Timestamp":"2026-05-17T14:34:32.3565252+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel-all","Scenario":"Subset-Hack+FiraCode+JetBrainsMono","DurationMs":3821,"DurationS":3.82,"Timestamp":"2026-05-17T14:34:36.3395668+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel-all","Scenario":"Subset-AlreadyInstalled","DurationMs":15,"DurationS":0.02,"Timestamp":"2026-05-17T14:34:36.4202031+02:00","Error":null,"Module":"1.0.33"}
{"Iteration":"1.0.33-pre007-parallel-all","Scenario":"All","DurationMs":221809,"DurationS":221.81,"Timestamp":"2026-05-17T14:38:20.2949322+02:00","Error":null,"Module":"1.0.33"}
Loading
Loading