The nupkg size has crept upwards over time, currently measuring nearly 6MB.
Impact:
- ourselves - MyGet choked on me (one option here would be to ditch MyGet entirely and use pure NuGet trusted publishing, which I've already done for Dapper and protobuf-net)
- our customers - slower restores
- NuGet - getting the size back down even a bit is PB/yr territory
Looking in package explorer, firstly: this is not the new analyzer, which weighs in at 106KB, but: the "lib" area
We currently ship:
- net10.0
- net8.0
- net6.0
- netstandard2.0
- net472
- net461
and we use embedded pdbs.
I propose 2 things:
- drop net6.0 (I don't think anyone will fight me here) and net461 (possibly more of a fight? maybe just say < net472 share netstandard2.0?)
- switch to snupkg and on-demand only
Delta
Measured locally at 89cf0c6, one clean pack per variant (wiping obj/bin between), so the rows are directly comparable:
| variant |
nupkg |
MB |
saved |
% |
snupkg |
| base (today) |
6,124,519 |
5.84 |
— |
— |
— |
| drop net6.0 |
5,116,918 |
4.88 |
1,007,601 |
16.5% |
— |
| drop net6.0 + net461 |
4,109,687 |
3.92 |
2,014,832 |
32.9% |
— |
| snupkg only |
4,499,458 |
4.29 |
1,625,061 |
26.5% |
1,619,898 |
| both proposals |
3,021,334 |
2.88 |
3,103,185 |
50.7% |
1,079,655 |
Doing both halves the package. For scale: 2.8.58 - the last v2 - was 3,069,353 bytes, so both proposals together put v3 back under the final v2 package, analyzer included.
Three things worth pulling out of that table:
- A TFM costs ~1MB, near enough exactly. net6.0 is 1,007,601 and net461 is 1,007,231. That is a useful constant for any future "can we add a TFM" conversation.
- The two changes overlap; they do not add up. snupkg saves 1,625,061 at six TFMs but only 1,088,353 at four, because dropping a TFM already takes its PDB with it. Sum of the parts is 3,639,893, actual combined saving is 3,103,185.
- If we publish the snupkg, total bytes shipped fall 33%, not 50.7%. 6,124,519 goes to 4,100,989 across both files. The 50.7% is what a consumer restores by default, which is the number that matters for the bandwidth argument - symbols are then pulled only by people who actually want them.
At a conservative 1M downloads/day the combined change is ~3.10 TB/day, ~1.13 PB/yr not sent.
Where the size actually is
Confirming the point above, by group, as compressed bytes in the package:
| group |
in-zip |
share |
| 6 x TFM (dll + xml doc) |
~1.0MB each |
98.6% |
| analyzer + codefixes |
71,651 |
1.2% |
| icon, nuspec, readme, props |
11,876 |
0.2% |
Each TFM is ~875KB of dll plus ~130KB of XML doc. One correction to my own note above: the 106KB I quoted from Package Explorer is the uncompressed size of a single assembly. In the package it is 71,651 bytes for both assemblies together, i.e. 1.2%. So the analyzer is even less of the story than I said.
Caveats on the TFM drops
I had the risk ordering backwards. net6.0 is the cheap one; net461 is the one that needs care.
net6.0 - a net6.0/net7.0 consumer resolves netstandard2.0, and UNIX_SOCKET is defined only on net6.0+. That symbol does not guard an optimisation, it guards endpoint parsing: !path, and !@name for the Linux abstract namespace. So dropping net6.0 silently removes the ability to configure a unix domain socket for anyone still on .NET 6 or 7. Both are EOL, so I think that is an acceptable trade - but it is a functional removal, not a no-op, and should be called out in the release notes rather than discovered.
net461 - this is not "share netstandard2.0". The netstandard2.0 asset is compiled with VECTOR_SAFE, because the condition is only != net461. The comment sitting above that #if explains exactly what happens when 4.6.1 runs the vectorised path:
Could not load file or assembly 'System.Numerics.Vectors, Version=4.1.3.0, ...' [...] the NuGet tools do not reliably (or even occasionally) produce the correct assembly-binding-redirect entries to fix this up
So dropping the net461 asset does not give those consumers a slower path, it gives them the path we documented as broken on that runtime, with a binding redirect we already established cannot be fixed up. Doing it safely means widening the exclusion so netstandard2.0 also drops VECTOR_SAFE, which deoptimises every netstandard2.0 consumer - Mono, Unity, older .NET Core - to buy back 1MB. Size impact of that change is negligible; the trade is correctness, not bytes.
That reframes proposal 1: drop net6.0 on its own is 16.5% for a documented, EOL-only functional loss. net461 is a further 16.4% but needs the VECTOR_SAFE question answered first.
RESPite
src/RESPite/RESPite.csproj carries the same six TFMs and is a dependency, so a consumer's restore pulls both packages. Whatever we decide here should apply to both, or we leave a good part of the win on the table.
Timing
Worth doing sooner rather than later, for a reason that is not about our own convenience. Microsoft.Extensions.Caching.StackExchangeRedis, Microsoft.AspNetCore.DataProtection.StackExchangeRedis and Microsoft.AspNetCore.SignalR.StackExchangeRedis all pin the same floors, and NuGet resolves a range to its lowest satisfying version - so those exact versions get restored forever. Across 2.0.513, 2.0.593, 2.2.4, 2.6.90, 2.6.122 and 2.7.27 that is 410,006,091 downloads, 35.5% of our all-time 1,156,357,612.
The current floor has been 2.7.27 since Caching 8.0.4, and neither 9.0.x nor 10.0.x moved it. Whenever it does move to a 3.x, that single version inherits about a third of our download volume overnight, at whatever size it happens to be. Being lean before that bump is worth a great deal more than being lean after it.
The nupkg size has crept upwards over time, currently measuring nearly 6MB.
Impact:
Looking in package explorer, firstly: this is not the new analyzer, which weighs in at 106KB, but: the "lib" area
We currently ship:
and we use embedded pdbs.
I propose 2 things:
Delta
Measured locally at 89cf0c6, one clean pack per variant (wiping
obj/binbetween), so the rows are directly comparable:Doing both halves the package. For scale: 2.8.58 - the last v2 - was 3,069,353 bytes, so both proposals together put v3 back under the final v2 package, analyzer included.
Three things worth pulling out of that table:
At a conservative 1M downloads/day the combined change is ~3.10 TB/day, ~1.13 PB/yr not sent.
Where the size actually is
Confirming the point above, by group, as compressed bytes in the package:
Each TFM is ~875KB of dll plus ~130KB of XML doc. One correction to my own note above: the 106KB I quoted from Package Explorer is the uncompressed size of a single assembly. In the package it is 71,651 bytes for both assemblies together, i.e. 1.2%. So the analyzer is even less of the story than I said.
Caveats on the TFM drops
I had the risk ordering backwards. net6.0 is the cheap one; net461 is the one that needs care.
net6.0 - a net6.0/net7.0 consumer resolves
netstandard2.0, andUNIX_SOCKETis defined only on net6.0+. That symbol does not guard an optimisation, it guards endpoint parsing:!path, and!@namefor the Linux abstract namespace. So dropping net6.0 silently removes the ability to configure a unix domain socket for anyone still on .NET 6 or 7. Both are EOL, so I think that is an acceptable trade - but it is a functional removal, not a no-op, and should be called out in the release notes rather than discovered.net461 - this is not "share netstandard2.0". The
netstandard2.0asset is compiled withVECTOR_SAFE, because the condition is only!= net461. The comment sitting above that#ifexplains exactly what happens when 4.6.1 runs the vectorised path:So dropping the net461 asset does not give those consumers a slower path, it gives them the path we documented as broken on that runtime, with a binding redirect we already established cannot be fixed up. Doing it safely means widening the exclusion so
netstandard2.0also dropsVECTOR_SAFE, which deoptimises every netstandard2.0 consumer - Mono, Unity, older .NET Core - to buy back 1MB. Size impact of that change is negligible; the trade is correctness, not bytes.That reframes proposal 1: drop net6.0 on its own is 16.5% for a documented, EOL-only functional loss. net461 is a further 16.4% but needs the
VECTOR_SAFEquestion answered first.RESPite
src/RESPite/RESPite.csprojcarries the same six TFMs and is a dependency, so a consumer's restore pulls both packages. Whatever we decide here should apply to both, or we leave a good part of the win on the table.Timing
Worth doing sooner rather than later, for a reason that is not about our own convenience.
Microsoft.Extensions.Caching.StackExchangeRedis,Microsoft.AspNetCore.DataProtection.StackExchangeRedisandMicrosoft.AspNetCore.SignalR.StackExchangeRedisall pin the same floors, and NuGet resolves a range to its lowest satisfying version - so those exact versions get restored forever. Across 2.0.513, 2.0.593, 2.2.4, 2.6.90, 2.6.122 and 2.7.27 that is 410,006,091 downloads, 35.5% of our all-time 1,156,357,612.The current floor has been 2.7.27 since Caching 8.0.4, and neither 9.0.x nor 10.0.x moved it. Whenever it does move to a 3.x, that single version inherits about a third of our download volume overnight, at whatever size it happens to be. Being lean before that bump is worth a great deal more than being lean after it.