Skip to content

Packaging: to snupkg or not to snupkg? (also: TFMs) #3184

Description

@mgravell

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:

  1. 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?)
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions