How to use zram as tmpfs on NixOS

I propose these as for know suggested changes for interface, if it gets more than 5 upvote I attempt to make it

I didn’t check for spell errors, syntax errors, lint or any other thing yet, but I think it’s a good start,

All sort of suggestions is welcomed, suggestions to improve, nix mistake or better way to do things, …

@TLATER @nrdxp @YorikSar @ElvishJerricco @Solene @Sid

  perDevOptions = {
    options = {
      memoryPercent = mkOption { # (0,100)
        default = 50;
        example = 90;
        type = types.int;
        description = lib.mdDoc ''
          Maximum total amount of memory that can be stored in the zram swap devices
          (as a percentage of your total memory). Defaults to 1/2 of your total
          RAM. Run `zramctl` to check how good your zram memory device is compressed.
          This doesn't define how much memory will be used by the zram swap devices.
        '';
      };
      memoryMax = mkOption { # (0,maxRam)
        default = null;
        example = 4000000000;
        type = with types; nullOr int;
        description = lib.mdDoc ''
          Maximum total amount of memory (in bytes) that can be stored in the zram
          swap devices.
          This doesn't define how much memory will be used by the zram swap devices.
          default is null which equals to memory percent as the only limit
        '';
      };
      priority = mkOption { # 5: warning if any swap and less than disk swap
        # todo: same warning for other manual swaps as well
        default = 5;
        type = types.int;
        description = lib.mdDoc ''
          Priority of the zram swap devices. It should be a number higher than
          the priority of your disk-based swap devices (so that the system will
          fill the zram swap devices before falling back to disk swap).
        '';
      };
      algorithm = mkOption {
        default = "zstd";
        example = "lz4";
        type = with types; either (enum [ "lzo" "lz4" "zstd" "lzo-rle" "lz4hc" "842" ]) str;

        description = lib.mdDoc ''
          Compression algorithm. `lzo` has good compression,
          but is slow. `lz4` has bad compression, but is fast.
          `zstd` is both good compression and fast, but requires newer kernel.
          You can check what other algorithms are supported by your zram device with
          {command}`cat /sys/class/block/zram*/comp_algorithm` (this command shows, 
          per zram device available algorithms, therefore you can see the working algorithm
          of each zram device wrapped in brackets)
        '';
      };
    };
  };  

  options = {

    zramSwap = {

      enable = mkOption {
        default = false;
        type = types.bool;
        description = lib.mdDoc ''
          Enable in-memory compressed devices and swap space provided by the zram
          kernel module.
          See [
            https://www.kernel.org/doc/Documentation/blockdev/zram.txt
          ](https://www.kernel.org/doc/Documentation/blockdev/zram.txt).
        '';
      };

      swapCount = mkOption {
        default = 1;
        example = 0;
        type = types.int;
        description = lib.mdDoc ''
          Number of zram devices to be used as swap. Must be
          `<= zramSwap.devCount`; default and recommended is 1.
        '';
      };

      devCount = mkOption { #(if null equal to swapCount) (swapCount,upperlimit)
        default = null;
        example = 1;
        type = with types; nullOr int;
        description = lib.mdDoc ''
          Number of zram device(s) to create. 
          Default is null which means, set equal to `zramSwap.swapCount`
          See also
          `zramSwap.swapCount`
        '';
      };
      
      swapConfig = mkOption (perDevOptions ++ 
        {
          useAsSwap = mkOption {
            default = false;
            example = true;
            type = types.bool;
            description = lib.mdDoc ''
              Make the zram device to be used as swap memory.
              By default, explicitly defined zram devices wouldn't be
              used as ram, if defined
            '';
          };
        }
      );

      device = mkOption {
        type = types.attrsOf (types.submodule perDevOptions);
        description = lib.mdDoc ''
          Configuration options for zram device(s)
        '';
        example = literalExpression ''
          { 
            "2" = { 
              memoryPercent = 10;
              priority = 4;
            };
          }
        '';
      };
    };
  };

perDevOptions would be defined in let section

by refrence, original interface


  options = {

    zramSwap = {

      enable = mkOption {
        default = false;
        type = types.bool;
        description = lib.mdDoc ''
          Enable in-memory compressed devices and swap space provided by the zram
          kernel module.
          See [
            https://www.kernel.org/doc/Documentation/blockdev/zram.txt
          ](https://www.kernel.org/doc/Documentation/blockdev/zram.txt).
        '';
      };

      numDevices = mkOption {
        default = 1;
        type = types.int;
        description = lib.mdDoc ''
          Number of zram devices to create. See also
          `zramSwap.swapDevices`
        '';
      };

      swapDevices = mkOption {
        default = null;
        example = 1;
        type = with types; nullOr int;
        description = lib.mdDoc ''
          Number of zram devices to be used as swap. Must be
          `<= zramSwap.numDevices`.
          Default is same as `zramSwap.numDevices`, recommended is 1.
        '';
      };

      memoryPercent = mkOption {
        default = 50;
        type = types.int;
        description = lib.mdDoc ''
          Maximum total amount of memory that can be stored in the zram swap devices
          (as a percentage of your total memory). Defaults to 1/2 of your total
          RAM. Run `zramctl` to check how good memory is compressed.
          This doesn't define how much memory will be used by the zram swap devices.
        '';
      };

      memoryMax = mkOption {
        default = null;
        type = with types; nullOr int;
        description = lib.mdDoc ''
          Maximum total amount of memory (in bytes) that can be stored in the zram
          swap devices.
          This doesn't define how much memory will be used by the zram swap devices.
        '';
      };

      priority = mkOption {
        default = 5;
        type = types.int;
        description = lib.mdDoc ''
          Priority of the zram swap devices. It should be a number higher than
          the priority of your disk-based swap devices (so that the system will
          fill the zram swap devices before falling back to disk swap).
        '';
      };

      algorithm = mkOption {
        default = "zstd";
        example = "lz4";
        type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
        description = lib.mdDoc ''
          Compression algorithm. `lzo` has good compression,
          but is slow. `lz4` has bad compression, but is fast.
          `zstd` is both good compression and fast, but requires newer kernel.
          You can check what other algorithms are supported by your zram device with
          {command}`cat /sys/class/block/zram*/comp_algorithm`
        '';
      };
    };

  };
1 Like

I don’t think it makes much sense to keep swapCount if we have swapConfig.

swapConfig should probably be a list of zramSwapOptions instead. I don’t think it makes much sense to name the swap devices.

I don’t get how non-Swap devices are supposed to work. Like, how would you use them? You don’t know which one you get, so it’s not like you could do anything with them declaratively.
Perhaps an interface to declaratively do something with the swap device after creation could make them useful. Swap would be a specialisation of that interface.

What do you think of this instead:

{
  zram.devices = let
    test-ext4 = {
      fileSystem = {
        fsType = "ext4";
        options = "foo=bar";
        mountPoint = "/foo";
        label = "test-ext4";
        autoFormat = true;
      };
      algorithm = "zstd";
      memoryMax = 1073741824;
    };
    swap = {
      swapDevice = {
        priority = 5;
        label = "zramSwap";
      };
      algorithm = "zstd";
      memoryPercent = 80;
    };
  in [
    test-ext4
    swap
  ];
}

swapDevice and fileSystem would bear the options of swapDevices and filesystems respectively and would pass them through to the respective original options.

The zram option would only include options relevant to zram configuration and delegate everything else to the specialised options.

At that point named submodules might make sense again, i.e.

{
  zram.devices = {
    test-ext4 = {
      fileSystem = {
        fsType = "ext4";
        options = "foo=bar";
        mountPoint = "/foo";
        label = "test-ext4";
        autoFormat = true;
      };
      algorithm = "zstd";
      memoryMax = 1073741824;
    };
    swap = {
      swapDevice = {
        priority = 5;
        label = "zramSwap";
      };
      algorithm = "zstd";
      memoryPercent = 80;
    };
  };
}

We could actually still keep the old zramSwap option this way and simply make it use the new option internally.

1 Like

About the first clause:

I don’t think it makes much sense to keep swapCount if we have swapConfig .

The purpose of swapConfig, and swapCount should be traced-back into old kernels and a still living habit of creating multiple zram devices (due to legacy reason of multi thread access to a device), usually with identical configuration. Something like:

zramSwap.device."0" = {
  useAsSwap = true;
  algorithm = "lz4"
}
zramSwap.device."1" = {
  useAsSwap = true;
  algorithm = "lz4"
}
zramSwap.device."2" = {
  useAsSwap = true;
  algorithm = "lz4"
}
zramSwap.device."3" = {
  useAsSwap = true;
  algorithm = "lz4"
}
....

So with those options (that something similar to it right now exist as well, but not per device, but for all devices [and not even fully as we see about compression algorithm]). It can be set like

zramSwap.swapCount = 12;
zramSwap.swapConfig = {
  algorithm = "lz4";
};

With this separation, users can set two diff compression algorithms for, let’s say, the ram device used as tmpfs and ram device as zram swap, also, if they want to set diff compression algorithm for any specific device, they can do it on a per device manner, something that is not possible to be done with current interface without writing extra nix modules, sth like

zramSwap.swapCount = 4;
zramSwap.device."4" = {
  useAsSwap = false;
  algorithm = "lz4";
};

in this config I set device 0-3 as swap with default configuration and use device 4 as some random block devices with 50% of memory as limit (similar to other blocks) and setting a special compression algorithm for it
As far as I saw it’s supported in zram, and it’s just a matter of writing the code to support it nix as well

Surely swapCount can move inside swapConfig for more convenience
therefore, all devices allocated by swapCount (0,n-1) would not be accessible by device (throwing a nix error). So my vision for zram is, 90 percent of users just want to get their swapping done, and 30 percent of them love to create many of them, so swapConfig and swapCount make it easy for them.
On the other hand, device module make it possible for other 10% users to do other weird things with their zram devices in their own specific way e.g.: setting special option for them and delegate the mounting of them to file system or any other untaught weird ways we can use it (surely these percentages are a very rough estimation)
This way, we can keep both per device config and commonly used special case of people well-supported

But on the other hand, you are right about the fact that the chance of nix maintainers accepting a breaking change is less than noticing a flying saucer in sky, so my bet on changing interface is a bad one

I also hate the zramSwap name and I think it should be changed to simply zram as its more semantic, easier, and generally make a lot of more sense. But I think that would surely be a big debate that I prefer to keep for some other time

The only change that can be done with current interface is, fixing the difference of default compression algorithm problem (that nix does not set zram devices compression that are not swap device, even worse, it does set them to inferior lzo-rle algorithm that is not changeable through any nix-ish way), tho this fix can be done even with current active interface, so that, default of all the new devices created be synced to zstd (not just swap ones) and devices not used as swap obey the compression algorithm set in config as well, but we lose the per device customization.

After reading your modules more carefully and understanding your proposal fully (I’m a nix noob), I would try to respond to each one of your points one by one in next post

swapConfig should probably be a list of zramSwapOptions instead. I don’t think it makes much sense to name the swap devices.

I didn’t mean to name them, maybe I wrote sth wrong in nix file that means that or I’m not getting your point