Repository: armadaproject/armada
Pull request #2552 (2023-06-12 09:57:07)

NodeDb performance improvements
The current NodeDb implementation effectively iterates over groups of nodes, where all nodes in a group have an equal amount  allocatable of some resource, e.g., CPU. This causes issues in fragmented clusters, where all nodes may differ in amount of allocatable resources.  With this PR, we introduce two optimisations:

1. Reducing the overhead associated with each group.
2. Bucketing nodes with similar amounts of allocatable resources, e.g., all nodes with [2, 3) CPU allocatable, thus reducing the number of groups.

The overall speedup for a particular benchmark of interest is about a factor 10x. It also significantly reduces allocations, which will reduce GC pressure.

Current:
```
> go.exe test -benchmem -run=^$ -bench ^BenchmarkNodeTypeIterator$ github.com/armadaproject/armada/internal/scheduler/nodedb
goos: windows
goarch: amd64
pkg: github.com/armadaproject/armada/internal/scheduler/nodedb
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
BenchmarkNodeTypeIterator-20               39607             29767 ns/op           21900 B/op        698 allocs/op
PASS
ok      github.com/armadaproject/armada/internal/scheduler/nodedb       1.652s
```

New:
```
> go.exe test -benchmem -run=^$ -bench ^BenchmarkNodeTypeIterator$ github.com/armadaproject/armada/internal/scheduler/nodedb
goos: windows
goarch: amd64
pkg: github.com/armadaproject/armada/internal/scheduler/nodedb
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
BenchmarkNodeTypeIterator-20              401348              2810 ns/op            1376 B/op         34 allocs/op
PASS
ok      github.com/armadaproject/armada/internal/scheduler/nodedb       1.324s
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) Report
Patch coverage: **`76.82`**% and project coverage change: **`-0.01`** :warning:
> Comparison is base [(`08634b1`)](https://app.codecov.io/gh/armadaproject/armada/commit/08634b13778e8a85dde88f9c5c14d6a328e4a85b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 58.37% compared to head [(`07420bf`)](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) 58.37%.

> :exclamation: Current head 07420bf differs from pull request most recent head 7c2ba76. Consider uploading reports for the commit 7c2ba76 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2552      +/-   ##
==========================================
- Coverage   58.37%   58.37%   -0.01%     
==========================================
  Files         236      236              
  Lines       30080    30104      +24     
==========================================
+ Hits        17558    17572      +14     
- Misses      11177    11185       +8     
- Partials     1345     1347       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| armada-server | `58.37% <76.82%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject) | Coverage Δ | |
|---|---|---|
| [internal/armada/server/lease.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9sZWFzZS5nbw==) | `7.70% <ø> (+0.03%)` | :arrow_up: |
| [internal/armada/server/submit\_to\_log.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvYXJtYWRhL3NlcnZlci9zdWJtaXRfdG9fbG9nLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/scheduler/pool\_assigner.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3Bvb2xfYXNzaWduZXIuZ28=) | `72.72% <ø> (ø)` | |
| [internal/scheduler/schedulerobjects/node.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvbm9kZS5nbw==) | `0.00% <0.00%> (ø)` | |
| [internal/scheduler/submitcheck.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3N1Ym1pdGNoZWNrLmdv) | `73.74% <ø> (ø)` | |
| [internal/scheduler/nodedb/nodedb.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL25vZGVkYi9ub2RlZGIuZ28=) | `59.06% <71.62%> (-0.90%)` | :arrow_down: |
| [internal/scheduler/schedulerobjects/quantity.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvcXVhbnRpdHkuZ28=) | `91.30% <80.00%> (-8.70%)` | :arrow_down: |
| [internal/scheduler/nodedb/nodeiteration.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL25vZGVkYi9ub2RlaXRlcmF0aW9uLmdv) | `72.65% <81.39%> (+0.18%)` | :arrow_up: |
| [internal/scheduler/schedulerobjects/nodetype.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxlcm9iamVjdHMvbm9kZXR5cGUuZ28=) | `78.00% <95.00%> (+1.15%)` | :arrow_up: |
| [internal/scheduler/scheduling\_algo.go](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject#diff-aW50ZXJuYWwvc2NoZWR1bGVyL3NjaGVkdWxpbmdfYWxnby5nbw==) | `71.82% <100.00%> (+0.51%)` | :arrow_up: |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/armadaproject/armada/pull/2552?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=armadaproject).


Patch:
diff --git a/config/armada/config.yaml b/config/armada/config.yaml
index a90597b9..265c2d80 100644
--- a/config/armada/config.yaml
+++ b/config/armada/config.yaml
@@ -90,8 +90,10 @@ scheduling:
   minJobResources:
     memory: 1Mi
   indexedResources:
-    - cpu
-    - memory
+    - name: "cpu"
+      resolution: "100m"
+    - name: "memory"
+      resolution: "1Mi"
   minTerminationGracePeriod: 1s
   maxTerminationGracePeriod: 300s
 queueManagement:
diff --git a/internal/armada/configuration/types.go b/internal/armada/configuration/types.go
index 2b5d8c81..a1a23002 100644
--- a/internal/armada/configuration/types.go
+++ b/internal/armada/configuration/types.go
@@ -7,6 +7,7 @@ import (
 	"github.com/go-redis/redis"
 	"golang.org/x/exp/slices"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 
 	authconfig "github.com/armadaproject/armada/internal/common/auth/configuration"
 	grpcconfig "github.com/armadaproject/armada/internal/common/grpc/configuration"
@@ -129,7 +130,7 @@ type SchedulingConfig struct {
 	// Resources, e.g., "cpu", "memory", and "nvidia.com/gpu",
 	// for which the scheduler creates indexes for efficient lookup.
 	// Applies only to the new scheduler.
-	IndexedResources []string
+	IndexedResources []IndexedResource
 	// Node labels that the scheduler creates indexes for efficient lookup of.
 	// Should include node labels frequently used for scheduling.
 	// Since the scheduler can efficiently sort out nodes for which these labels
@@ -186,6 +187,13 @@ type SchedulingConfig struct {
 	AlwaysAttemptScheduling bool
 }
 
+type IndexedResource struct {
+	// Resource name. E.g., "cpu", "memory", or "nvidia.com/gpu".
+	Name string
+	// See NodeDb docs.
+	Resolution resource.Quantity
+}
+
 // NewSchedulerConfig stores config for the new Pulsar-based scheduler.
 // This scheduler will eventually replace the current scheduler.
 type NewSchedulerConfig struct {
diff --git a/internal/armada/server/lease.go b/internal/armada/server/lease.go
index 4065299a..f8641d32 100644
--- a/internal/armada/server/lease.go
+++ b/internal/armada/server/lease.go
@@ -382,14 +382,10 @@ func (q *AggregatedQueueServer) getJobs(ctx context.Context, req *api.StreamingL
 		}
 		nodes = append(nodes, node)
 	}
-	indexedResources := q.schedulingConfig.IndexedResources
-	if len(indexedResources) == 0 {
-		indexedResources = []string{"cpu", "memory"}
-	}
 	nodeDb, err := nodedb.NewNodeDb(
 		q.schedulingConfig.Preemption.PriorityClasses,
 		q.schedulingConfig.MaxExtraNodesToConsider,
-		indexedResources,
+		q.schedulingConfig.IndexedResources,
 		q.schedulingConfig.IndexedTaints,
 		q.schedulingConfig.IndexedNodeLabels,
 	)
diff --git a/internal/armada/server/submit_to_log.go b/internal/armada/server/submit_to_log.go
index 16c00246..08f2002b 100644
--- a/internal/armada/server/submit_to_log.go
+++ b/internal/armada/server/submit_to_log.go
@@ -106,7 +106,7 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 	}
 
 	// Convert the API jobs to log jobs.
-	responses := make([]*api.JobSubmitResponseItem, len(req.JobRequestItems), len(req.JobRequestItems))
+	responses := make([]*api.JobSubmitResponseItem, len(req.JobRequestItems))
 
 	originalIds, err := srv.getOriginalJobIds(ctx, apiJobs)
 	if err != nil {
@@ -157,8 +157,7 @@ func (srv *PulsarSubmitServer) SubmitJobs(ctx context.Context, req *api.JobSubmi
 
 		// Try converting the log job back to an API job to make sure there are no errors.
 		// The log consumer will do this again; we do it here to ensure that any errors are noticed immediately.
-		_, err = eventutil.ApiJobFromLogSubmitJob(userId, groups, req.Queue, req.JobSetId, time.Now(), logJob)
-		if err != nil {
+		if _, err := eventutil.ApiJobFromLogSubmitJob(userId, groups, req.Queue, req.JobSetId, time.Now(), logJob); err != nil {
 			return nil, err
 		}
 
diff --git a/internal/scheduler/constraints/constraints.go b/internal/scheduler/constraints/constraints.go
index 66bc4a3d..32c5f44f 100644
--- a/internal/scheduler/constraints/constraints.go
+++ b/internal/scheduler/constraints/constraints.go
@@ -1,7 +1,10 @@
 package constraints
 
 import (
+	"math"
+
 	"github.com/pkg/errors"
+	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
@@ -88,7 +91,7 @@ func SchedulingConstraintsFromSchedulingConfig(
 func absoluteFromRelativeLimits(totalResources schedulerobjects.ResourceList, relativeLimits map[string]float64) schedulerobjects.ResourceList {
 	absoluteLimits := schedulerobjects.NewResourceList(len(relativeLimits))
 	for t, f := range relativeLimits {
-		absoluteLimits.Set(t, schedulerobjects.ScaleQuantity(totalResources.Get(t).DeepCopy(), f))
+		absoluteLimits.Set(t, ScaleQuantity(totalResources.Get(t).DeepCopy(), f))
 	}
 	return absoluteLimits
 }
@@ -142,3 +145,11 @@ func (constraints *SchedulingConstraints) CheckPerQueueAndPriorityClassConstrain
 func exceedsResourceLimits(used, limits schedulerobjects.ResourceList) bool {
 	return !used.IsStrictlyLessOrEqual(limits)
 }
+
+// ScaleQuantity scales q in-place by a factor f.
+// This functions overflows for quantities the milli value of which can't be expressed as an int64.
+// E.g., 1Pi is ok, but not 10Pi.
+func ScaleQuantity(q resource.Quantity, f float64) resource.Quantity {
+	q.SetMilli(int64(math.Round(float64(q.MilliValue()) * f)))
+	return q
+}
diff --git a/internal/scheduler/nodedb/encoding.go b/internal/scheduler/nodedb/encoding.go
new file mode 100644
index 00000000..682b34c0
--- /dev/null
+++ b/internal/scheduler/nodedb/encoding.go
@@ -0,0 +1,75 @@
+package nodedb
+
+import (
+	"encoding/binary"
+
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+// NodeIndexKey returns a []byte to be used as a key with the NodeIndex memdb index with layout
+//
+// 0            8              16             32
+// | nodeTypeId | resources[0] | resources[1] | ... |
+//
+// where the numbers indicate number of bytes.
+//
+// The key layout is such that an index ordered first by the nodeTypeId, then resources[0], and so on.
+// The byte representation is appended to out, which is returned.
+func NodeIndexKey(out []byte, nodeTypeId uint64, resources []resource.Quantity) []byte {
+	out = EncodeUint64(out, nodeTypeId)
+	for _, q := range resources {
+		out = EncodeQuantity(out, q)
+	}
+	return out
+}
+
+// RoundedNodeIndexKeyFromResourceList works like NodeIndexKey, except that prior to constructing a the key
+// the i-th resource is rounded down to the closest multiple of resourceResolutionMillis[i].
+// It also takes as arguments a list of resource names and a resourceList, instead of a list of resources.
+func RoundedNodeIndexKeyFromResourceList(out []byte, nodeTypeId uint64, resourceNames []string, resourceResolutionMillis []int64, rl schedulerobjects.ResourceList) []byte {
+	out = EncodeUint64(out, nodeTypeId)
+	for i, name := range resourceNames {
+		resolution := resourceResolutionMillis[i]
+		q := rl.Get(name)
+		q.SetMilli((q.MilliValue() / resolution) * resolution)
+		out = EncodeQuantity(out, q)
+	}
+	return out
+}
+
+// EncodeQuantity returns the canonical byte representation of a resource.Quantity used within the nodeDb.
+// The resulting []byte is such that for two resource.Quantity a and b, a.Cmp(b) = bytes.Compare(enc(a), enc(b)).
+// The byte representation is appended to out, which is returned.
+func EncodeQuantity(out []byte, val resource.Quantity) []byte {
+	// We assume that any quantity we want to compare can be represented as an int64.
+	return EncodeInt64(out, val.MilliValue())
+}
+
+// EncodeInt64 returns the canonical byte representation of a int64 used within the nodeDb.
+// The resulting []byte is such that for two int64 a and b, a.Cmp(b) = bytes.Compare(enc(a), enc(b)).
+// The byte representation is appended to out, which is returned.
+func EncodeInt64(out []byte, val int64) []byte {
+	size := 8
+	out = append(out, make([]byte, size)...)
+
+	// This bit flips the usign bit on any sized signed twos-complement integer,
+	// which when truncated to a uint of the same size will bias the value such
+	// that the maximum negative int becomes 0, and the maximum positive int
+	// becomes the maximum positive uint.
+	scaled := val ^ int64(-1<<(size*8-1))
+
+	binary.BigEndian.PutUint64(out[len(out)-8:], uint64(scaled))
+	return out
+}
+
+// EncodeUint64 returns the canonical byte representation of a uint64 used within the nodeDb.
+// The resulting []byte is such that for two uint64 a and b, a.Cmp(b) = bytes.Compare(enc(a), enc(b)).
+// The byte representation is appended to out, which is returned.
+func EncodeUint64(out []byte, val uint64) []byte {
+	size := 8
+	out = append(out, make([]byte, size)...)
+	binary.BigEndian.PutUint64(out[len(out)-size:], val)
+	return out
+}
diff --git a/internal/scheduler/nodedb/nodedb.go b/internal/scheduler/nodedb/nodedb.go
index 6af078c0..41c48b66 100644
--- a/internal/scheduler/nodedb/nodedb.go
+++ b/internal/scheduler/nodedb/nodedb.go
@@ -17,6 +17,7 @@ import (
 
 	"github.com/armadaproject/armada/internal/armada/configuration"
 	"github.com/armadaproject/armada/internal/common/armadaerrors"
+	"github.com/armadaproject/armada/internal/common/util"
 	schedulerconfig "github.com/armadaproject/armada/internal/scheduler/configuration"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -26,11 +27,9 @@ import (
 // This helps avoid scheduling new jobs onto nodes that make it impossible to re-schedule evicted jobs.
 const evictedPriority int32 = -1
 
-// NodeDb is the scheduler-internal system for storing node information.
-// It's used to efficiently find nodes on which a pod can be scheduled.
+// NodeDb is the scheduler-internal system used to efficiently find nodes on which a pod could be scheduled.
 type NodeDb struct {
-	// In-memory database. Stores *SchedulerNode.
-	// Used to efficiently iterate over nodes in sorted order.
+	// In-memory database storing *schedulerobjects.Node.
 	db *memdb.MemDB
 	// Once a node has been found on which a pod can be scheduled,
 	// the NodeDb will consider up to the next maxExtraNodesToConsider nodes.
@@ -55,6 +54,14 @@ type NodeDb struct {
 	indexedResources []string
 	// Like indexedResources, but stored as a map for efficient lookup.
 	indexedResourcesSet map[string]interface{}
+	// The resolution with which indexed resources are tracked. In the same order as indexedResources.
+	// For example, if indexedResources = []string{"cpu"} and indexedResourceResolutionMillis = []int64{1000},
+	// then nodes with, e.g., 2000, 2100, and 2900 mCPU allocatable are all registered as having 2000 mCPU allocatable.
+	//
+	// Lower resolution makes scheduling faster, but may lead to jobs incorrectly being considered unschedulable.
+	indexedResourceResolutionMillis []int64
+	// Map from priority class priority to the index tracking allocatable resources at that priority.
+	indexNameByPriority map[int32]string
 	// Taint keys that to create indexes for.
 	// Should include taints frequently used for scheduling.
 	// Since the NodeDb can efficiently sort out nodes with taints not tolerated
@@ -72,12 +79,12 @@ type NodeDb struct {
 	// Total number of nodes in the db.
 	numNodes int
 	// Number of nodes in the db by node type.
-	numNodesByNodeType map[string]int
+	numNodesByNodeType map[uint64]int
 	// Total amount of resources, e.g., "cpu", "memory", "gpu", across all nodes in the db.
 	totalResources schedulerobjects.ResourceList
 	// Set of node types. Populated automatically as nodes are inserted.
 	// Node types are not cleaned up if all nodes of that type are removed from the NodeDb.
-	nodeTypes map[string]*schedulerobjects.NodeType
+	nodeTypes map[uint64]*schedulerobjects.NodeType
 	// Map from podRequirementsNotMetReason Sum64() to the string representation of that reason.
 	// Used to avoid allocs.
 	podRequirementsNotMetReasonStringCache map[uint64]string
@@ -88,7 +95,7 @@ type NodeDb struct {
 func NewNodeDb(
 	priorityClasses map[string]configuration.PriorityClass,
 	maxExtraNodesToConsider uint,
-	indexedResources,
+	indexedResources []configuration.IndexedResource,
 	indexedTaints,
 	indexedNodeLabels []string,
 ) (*NodeDb, error) {
@@ -99,10 +106,16 @@ func NewNodeDb(
 	prioritiesToTryAssigningAt := maps.Keys(allowedPriorities)
 	slices.Sort(prioritiesToTryAssigningAt)
 
-	db, err := memdb.NewMemDB(nodeDbSchema(
-		prioritiesToTryAssigningAt,
-		indexedResources,
-	))
+	if len(indexedResources) == 0 {
+		return nil, errors.WithStack(&armadaerrors.ErrInvalidArgument{
+			Name:    "indexedResources",
+			Value:   indexedResources,
+			Message: "there must be at least one index resource",
+		})
+	}
+	indexedResourceNames := util.Map(indexedResources, func(v configuration.IndexedResource) string { return v.Name })
+	schema, indexNameByPriority := nodeDbSchema(prioritiesToTryAssigningAt, indexedResourceNames)
+	db, err := memdb.NewMemDB(schema)
 	if err != nil {
 		return nil, errors.WithStack(err)
 	}
@@ -133,14 +146,19 @@ func NewNodeDb(
 		priorityClasses:            priorityClasses,
 		prioritiesToTryAssigningAt: prioritiesToTryAssigningAt,
 		maxExtraNodesToConsider:    maxExtraNodesToConsider,
-		indexedResources:           slices.Clone(indexedResources),
-		indexedResourcesSet:        mapFromSlice(indexedResources),
-		indexedTaints:              mapFromSlice(indexedTaints),
-		indexedNodeLabels:          mapFromSlice(indexedNodeLabels),
-		nodeTypes:                  make(map[string]*schedulerobjects.NodeType),
-		numNodesByNodeType:         make(map[string]int),
-		totalResources:             schedulerobjects.ResourceList{Resources: make(map[string]resource.Quantity)},
-		db:                         db,
+		indexedResources:           indexedResourceNames,
+		indexedResourcesSet:        mapFromSlice(indexedResourceNames),
+		indexedResourceResolutionMillis: util.Map(
+			indexedResources,
+			func(v configuration.IndexedResource) int64 { return v.Resolution.MilliValue() },
+		),
+		indexNameByPriority: indexNameByPriority,
+		indexedTaints:       mapFromSlice(indexedTaints),
+		indexedNodeLabels:   mapFromSlice(indexedNodeLabels),
+		nodeTypes:           make(map[uint64]*schedulerobjects.NodeType),
+		numNodesByNodeType:  make(map[uint64]int),
+		totalResources:      schedulerobjects.ResourceList{Resources: make(map[string]resource.Quantity)},
+		db:                  db,
 		// Set the initial capacity (somewhat arbitrarily) to 128 reasons.
 		podRequirementsNotMetReasonStringCache: make(map[uint64]string, 128),
 	}, nil
@@ -158,7 +176,7 @@ func (nodeDb *NodeDb) String() string {
 	} else {
 		fmt.Fprint(w, "Node types:\n")
 		for _, nodeType := range nodeDb.nodeTypes {
-			fmt.Fprintf(w, "  %s\n", nodeType.Id)
+			fmt.Fprintf(w, "  %d\n", nodeType.Id)
 		}
 	}
 	w.Flush()
@@ -409,7 +427,7 @@ func (nodeDb *NodeDb) selectNodeForPodAtPriority(
 	priority int32,
 	req *schedulerobjects.PodRequirements,
 ) (*schedulerobjects.Node, error) {
-	nodeTypeIds := make([]string, len(pctx.MatchingNodeTypes))
+	nodeTypeIds := make([]uint64, len(pctx.MatchingNodeTypes))
 	for i, nodeType := range pctx.MatchingNodeTypes {
 		nodeTypeIds[i] = nodeType.Id
 	}
@@ -418,8 +436,18 @@ func (nodeDb *NodeDb) selectNodeForPodAtPriority(
 	for i, t := range nodeDb.indexedResources {
 		indexResourceRequests[i] = req.ResourceRequirements.Requests[v1.ResourceName(t)]
 	}
-
-	it, err := NewNodeTypesIterator(txn, nodeTypeIds, priority, nodeDb.indexedResources, indexResourceRequests)
+	indexName, ok := nodeDb.indexNameByPriority[priority]
+	if !ok {
+		return nil, errors.Errorf("no index for priority %d; must be in %v", priority, nodeDb.indexNameByPriority)
+	}
+	it, err := NewNodeTypesIterator(
+		txn,
+		nodeTypeIds,
+		indexName,
+		priority,
+		nodeDb.indexedResources,
+		indexResourceRequests,
+	)
 	if err != nil {
 		return nil, err
 	}
@@ -720,7 +748,12 @@ func (nodeDb *NodeDb) Upsert(node *schedulerobjects.Node) error {
 }
 
 func (nodeDb *NodeDb) UpsertWithTxn(txn *memdb.Txn, node *schedulerobjects.Node) error {
+	if len(node.AllocatableByPriorityAndResource) == 0 {
+		return errors.Errorf("can't upsert node with AllocatableByPriorityAndResource: %v", node.AllocatableByPriorityAndResource)
+	}
+
 	// Mutating the node once inserted is forbidden.
+	// TODO: We shouldn't need a copy here.
 	node = node.DeepCopy()
 
 	// Add an evictedPriority record to the node.
@@ -771,6 +804,12 @@ func (nodeDb *NodeDb) UpsertWithTxn(txn *memdb.Txn, node *schedulerobjects.Node)
 	node.NodeTypeId = nodeType.Id
 	node.NodeType = nodeType
 
+	// Compute the keys necessary to efficiently iterate over nodes.
+	node.NodeDbKeys = make([][]byte, len(nodeDb.prioritiesToTryAssigningAt))
+	for i, p := range nodeDb.prioritiesToTryAssigningAt {
+		node.NodeDbKeys[i] = nodeDb.nodeDbKeyFromNode(node.NodeDbKeys[i], node, p)
+	}
+
 	// Add the node to the db.
 	isNewNode := false
 	if existingNode, err := nodeDb.GetNodeWithTxn(txn, node.Id); err != nil {
@@ -804,52 +843,37 @@ func (nodeDb *NodeDb) ClearAllocated() error {
 	if err != nil {
 		return err
 	}
+	newNodes := make([]*schedulerobjects.Node, 0)
 	for node := it.NextNode(); node != nil; node = it.NextNode() {
 		node = node.DeepCopy()
 		node.AllocatableByPriorityAndResource = schedulerobjects.NewAllocatableByPriorityAndResourceType(
-			configuration.AllowedPriorities(nodeDb.priorityClasses),
-			nodeDb.totalResources,
+			nodeDb.prioritiesToTryAssigningAt,
+			node.TotalResources,
 		)
-		err := txn.Insert("nodes", node)
-		if err != nil {
-			return err
-		}
+		newNodes = append(newNodes, node)
+	}
+	if err := nodeDb.UpsertManyWithTxn(txn, newNodes); err != nil {
+		return err
 	}
 	txn.Commit()
 	return nil
 }
 
-func nodeDbSchema(priorities []int32, resources []string) *memdb.DBSchema {
+func nodeDbSchema(priorities []int32, resources []string) (*memdb.DBSchema, map[int32]string) {
 	indexes := make(map[string]*memdb.IndexSchema)
 	indexes["id"] = &memdb.IndexSchema{
 		Name:    "id",
 		Unique:  true,
 		Indexer: &memdb.StringFieldIndex{Field: "Id"},
 	}
-	for _, priority := range priorities {
-		resourceIndexes := []memdb.Indexer{
-			&memdb.StringFieldIndex{Field: "NodeTypeId"},
-		}
-		for _, resource := range resources {
-			resourceIndexes = append(
-				resourceIndexes,
-				&NodeAvailableResourceIndex{
-					Resource: resource,
-					Priority: priority,
-				},
-			)
-		}
-		resourceIndexes = append( // Tie-break by id.
-			resourceIndexes,
-			&memdb.StringFieldIndex{Field: "Id"},
-		)
-		name := nodeResourceIndexName(priority)
+	indexNameByPriority := make(map[int32]string, len(priorities))
+	for i, priority := range priorities {
+		name := nodeIndexName(i)
+		indexNameByPriority[priority] = name
 		indexes[name] = &memdb.IndexSchema{
-			Name:   name,
-			Unique: false,
-			Indexer: &memdb.CompoundIndex{
-				Indexes: resourceIndexes,
-			},
+			Name:    name,
+			Unique:  false,
+			Indexer: &NodeIndex{KeyIndex: i},
 		}
 	}
 	return &memdb.DBSchema{
@@ -859,11 +883,11 @@ func nodeDbSchema(priorities []int32, resources []string) *memdb.DBSchema {
 				Indexes: indexes,
 			},
 		},
-	}
+	}, indexNameByPriority
 }
 
-func nodeResourceIndexName(priority int32) string {
-	return fmt.Sprintf("%d", priority)
+func nodeIndexName(keyIndex int) string {
+	return fmt.Sprintf("%d", keyIndex)
 }
 
 // stringFromPodRequirementsNotMetReason returns the string representation of reason,
@@ -878,3 +902,16 @@ func (nodeDb *NodeDb) stringFromPodRequirementsNotMetReason(reason schedulerobje
 		return s
 	}
 }
+
+// nodeDbKeyFromNode returns the index key for a particular node and resource.
+// Allocatable resources are rounded down to the closest multiple of nodeDb.indexedResourceResolutionMillis.
+// This improves efficiency by reducing the number of distinct values in the index.
+func (nodeDb *NodeDb) nodeDbKeyFromNode(out []byte, node *schedulerobjects.Node, priority int32) []byte {
+	return RoundedNodeIndexKeyFromResourceList(
+		out,
+		node.NodeTypeId,
+		nodeDb.indexedResources,
+		nodeDb.indexedResourceResolutionMillis,
+		node.AllocatableByPriorityAndResource[priority],
+	)
+}
diff --git a/internal/scheduler/nodedb/nodeiteration.go b/internal/scheduler/nodedb/nodeiteration.go
index 87202668..731c3902 100644
--- a/internal/scheduler/nodedb/nodeiteration.go
+++ b/internal/scheduler/nodedb/nodeiteration.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/hashicorp/go-memdb"
 	"github.com/pkg/errors"
+	"golang.org/x/exp/slices"
 	"k8s.io/apimachinery/pkg/api/resource"
 
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
@@ -129,34 +130,24 @@ func (it *NodePairIterator) Next() interface{} {
 	return it.NextItem()
 }
 
-type NodeAvailableResourceIndex struct {
-	// Resource name, e.g., "cpu", "gpu", or "memory".
-	Resource string
-	// Job priority.
-	Priority int32
+// NodeIndex is an index for schedulerobjects.Node that returns node.NodeDbKeys[KeyIndex].
+type NodeIndex struct {
+	KeyIndex int
 }
 
 // FromArgs computes the index key from a set of arguments.
-// Takes a single argument resourceAmount of type resource.Quantity.
-func (index *NodeAvailableResourceIndex) FromArgs(args ...interface{}) ([]byte, error) {
+// Takes a single argument resourceAmount of type []byte.
+func (index *NodeIndex) FromArgs(args ...interface{}) ([]byte, error) {
 	if len(args) != 1 {
 		return nil, errors.New("must provide exactly one argument")
 	}
-	q, ok := args[0].(resource.Quantity)
-	if !ok {
-		return nil, errors.Errorf("expected Quantity, but got %T", args[0])
-	}
-	return schedulerobjects.EncodeQuantity(q), nil
+	return args[0].([]byte), nil
 }
 
 // FromObject extracts the index key from a *schedulerobjects.Node.
-func (index *NodeAvailableResourceIndex) FromObject(raw interface{}) (bool, []byte, error) {
-	node, ok := raw.(*schedulerobjects.Node)
-	if !ok {
-		return false, nil, errors.Errorf("expected *Node, but got %T", raw)
-	}
-	q := node.AvailableQuantityByPriorityAndResource(index.Priority, index.Resource)
-	return true, schedulerobjects.EncodeQuantity(q), nil
+func (index *NodeIndex) FromObject(raw interface{}) (bool, []byte, error) {
+	node := raw.(*schedulerobjects.Node)
+	return true, node.NodeDbKeys[index.KeyIndex], nil
 }
 
 // NodeTypesIterator is an iterator over all nodes of the given nodeTypes
@@ -164,20 +155,31 @@ func (index *NodeAvailableResourceIndex) FromObject(raw interface{}) (bool, []by
 // For example, all nodes of nodeType "foo" and "bar" with at least 2 cores and 1Gi memory allocatable at priority 2.
 // Nodes are returned in sorted order, from least to most of the specified resource available.
 type NodeTypesIterator struct {
-	priority                int32
-	indexedResources        []string
-	indexedResourceRequests []resource.Quantity
-	pq                      *nodeTypesIteratorPQ
+	pq *nodeTypesIteratorPQ
 }
 
-func NewNodeTypesIterator(txn *memdb.Txn, nodeTypeIds []string, priority int32, indexedResources []string, indexedResourceRequests []resource.Quantity) (*NodeTypesIterator, error) {
+func NewNodeTypesIterator(
+	txn *memdb.Txn,
+	nodeTypeIds []uint64,
+	indexName string,
+	priority int32,
+	indexedResources []string,
+	indexedResourceRequests []resource.Quantity,
+) (*NodeTypesIterator, error) {
 	pq := &nodeTypesIteratorPQ{
 		priority:         priority,
 		indexedResources: indexedResources,
 		items:            make([]*nodeTypesIteratorPQItem, 0, len(nodeTypeIds)),
 	}
 	for _, nodeTypeId := range nodeTypeIds {
-		it, err := NewNodeTypeIterator(txn, nodeTypeId, priority, indexedResources, indexedResourceRequests)
+		it, err := NewNodeTypeIterator(
+			txn,
+			nodeTypeId,
+			indexName,
+			priority,
+			indexedResources,
+			indexedResourceRequests,
+		)
 		if err != nil {
 			return nil, err
 		}
@@ -193,12 +195,7 @@ func NewNodeTypesIterator(txn *memdb.Txn, nodeTypeIds []string, priority int32,
 			it:   it,
 		})
 	}
-	return &NodeTypesIterator{
-		priority:                priority,
-		indexedResources:        indexedResources,
-		indexedResourceRequests: indexedResourceRequests,
-		pq:                      pq,
-	}, nil
+	return &NodeTypesIterator{pq: pq}, nil
 }
 
 func (it *NodeTypesIterator) WatchCh() <-chan struct{} {
@@ -255,13 +252,13 @@ func (it *nodeTypesIteratorPQ) less(a, b *schedulerobjects.Node) bool {
 	for _, t := range it.indexedResources {
 		qa := allocatableByPriorityA.Get(t)
 		qb := allocatableByPriorityB.Get(t)
-		cmp := qa.Cmp(qb)
-		if cmp == -1 {
+		if cmp := qa.Cmp(qb); cmp == -1 {
 			return true
 		} else if cmp == 1 {
 			return false
 		}
 	}
+	// Tie-break by id.
 	return a.Id < b.Id
 }
 
@@ -293,50 +290,69 @@ func (pq *nodeTypesIteratorPQ) Pop() any {
 // For example, all nodes of nodeType "foo" with at least 2 cores and 1Gi memory allocatable at priority 2.
 // Nodes are returned in sorted order, from least to most of the specified resource available.
 type NodeTypeIterator struct {
-	txn                     *memdb.Txn
-	nodeTypeId              string
-	priority                int32
-	indexedResources        []string
+	txn *memdb.Txn
+	// Only yield nodes of this nodeType.
+	nodeTypeId uint64
+	// Priority at which to consider allocatable resources on the node.
+	priority int32
+	// Name of the memdb index used for node iteration.
+	// Should correspond to the priority set for this iterator.
+	indexName string
+	// NodeDb indexed resources.
+	indexedResources []string
+	// Pod requests for indexed resources in the same order as indexedResources.
 	indexedResourceRequests []resource.Quantity
-	lowerBound              []resource.Quantity
-	memdbIterator           memdb.ResultIterator
-}
-
-func NewNodeTypeIterator(txn *memdb.Txn, nodeTypeId string, priority int32, indexedResources []string, indexedResourceRequests []resource.Quantity) (*NodeTypeIterator, error) {
+	// Current lower bound on node allocatable resources looked for.
+	// Updated in-place as the iterator makes progress.
+	lowerBound []resource.Quantity
+	// memdb key computed from nodeTypeId and lowerBound.
+	// Stored here to avoid dynamic allocs.
+	key []byte
+	// Current iterator into the underlying memdb.
+	// Updated in-place whenever lowerBound changes.
+	memdbIterator  memdb.ResultIterator
+	previousNodeId string
+}
+
+func NewNodeTypeIterator(
+	txn *memdb.Txn,
+	nodeTypeId uint64,
+	indexName string,
+	priority int32,
+	indexedResources []string,
+	indexedResourceRequests []resource.Quantity,
+) (*NodeTypeIterator, error) {
 	if len(indexedResources) != len(indexedResourceRequests) {
 		return nil, errors.Errorf("indexedResources and resourceRequirements are not of equal length")
 	}
-	memdbIterator, err := newNodeTypeIterator(txn, nodeTypeId, indexedResourceRequests, priority)
-	if err != nil {
-		return nil, err
-	}
-	return &NodeTypeIterator{
+	it := &NodeTypeIterator{
 		txn:                     txn,
 		nodeTypeId:              nodeTypeId,
 		priority:                priority,
+		indexName:               indexName,
 		indexedResources:        indexedResources,
 		indexedResourceRequests: indexedResourceRequests,
-		lowerBound:              make([]resource.Quantity, len(indexedResourceRequests)),
-		memdbIterator:           memdbIterator,
-	}, nil
+		lowerBound:              slices.Clone(indexedResourceRequests),
+	}
+	memdbIt, err := it.newNodeTypeIterator()
+	if err != nil {
+		return nil, err
+	}
+	it.memdbIterator = memdbIt
+	return it, nil
 }
 
-func newNodeTypeIterator(txn *memdb.Txn, nodeTypeId string, resourceRequirements []resource.Quantity, priority int32) (memdb.ResultIterator, error) {
-	args := make([]interface{}, 2+len(resourceRequirements))
-	args[0] = nodeTypeId
-	for i, q := range resourceRequirements {
-		args[i+1] = q
-	}
-	args[len(args)-1] = ""
-	it, err := txn.LowerBound(
+func (it *NodeTypeIterator) newNodeTypeIterator() (memdb.ResultIterator, error) {
+	it.key = NodeIndexKey(it.key[0:0], it.nodeTypeId, it.lowerBound)
+	memdbIt, err := it.txn.LowerBound(
 		"nodes",
-		nodeResourceIndexName(priority),
-		args...,
+		it.indexName,
+		it.key,
 	)
 	if err != nil {
 		return nil, errors.WithStack(err)
 	}
-	return it, nil
+	return memdbIt, nil
 }
 
 func (it *NodeTypeIterator) WatchCh() <-chan struct{} {
@@ -358,10 +374,18 @@ func (it *NodeTypeIterator) NextNode() (*schedulerobjects.Node, error) {
 			return nil, nil
 		}
 		node := v.(*schedulerobjects.Node)
-		if it.nodeTypeId != "" && node.NodeTypeId != it.nodeTypeId {
+		if node.Id == it.previousNodeId {
+			panic(fmt.Sprintf("iterator received the same node twice consecutively: %s", node.Id))
+		}
+		it.previousNodeId = node.Id
+		if node.NodeTypeId != it.nodeTypeId {
+			// There are no more nodes of this nodeType.
 			return nil, nil
 		}
 		allocatableByPriority := node.AllocatableByPriorityAndResource[it.priority]
+		if len(allocatableByPriority.Resources) == 0 {
+			return nil, errors.Errorf("node %s has no resources registered at priority %d: %v", node.Id, it.priority, node.AllocatableByPriorityAndResource)
+		}
 		for i, t := range it.indexedResources {
 			nodeQuantity := allocatableByPriority.Get(t)
 			requestQuantity := it.indexedResourceRequests[i]
@@ -373,7 +397,7 @@ func (it *NodeTypeIterator) NextNode() (*schedulerobjects.Node, error) {
 				for j := i; j < len(it.indexedResources); j++ {
 					it.lowerBound[j] = it.indexedResourceRequests[j]
 				}
-				memdbIterator, err := newNodeTypeIterator(it.txn, it.nodeTypeId, it.lowerBound, it.priority)
+				memdbIterator, err := it.newNodeTypeIterator()
 				if err != nil {
 					return nil, err
 				}
diff --git a/internal/scheduler/pool_assigner.go b/internal/scheduler/pool_assigner.go
index d399b434..14528c61 100644
--- a/internal/scheduler/pool_assigner.go
+++ b/internal/scheduler/pool_assigner.go
@@ -32,7 +32,7 @@ type DefaultPoolAssigner struct {
 	executorTimeout        time.Duration
 	priorityClasses        map[string]configuration.PriorityClass
 	priorities             []int32
-	indexedResources       []string
+	indexedResources       []configuration.IndexedResource
 	indexedTaints          []string
 	indexedNodeLabels      []string
 	poolByExecutorId       map[string]string
diff --git a/internal/scheduler/schedulerobjects/node.go b/internal/scheduler/schedulerobjects/node.go
index 059d268c..7dc79612 100644
--- a/internal/scheduler/schedulerobjects/node.go
+++ b/internal/scheduler/schedulerobjects/node.go
@@ -21,6 +21,7 @@ func (node *Node) DeepCopy() *Node {
 	return &Node{
 		Id:             node.Id,
 		Name:           node.Name,
+		NodeDbKeys:     slices.Clone(node.NodeDbKeys),
 		LastSeen:       node.LastSeen,
 		NodeType:       node.NodeType.DeepCopy(),
 		NodeTypeId:     node.NodeTypeId,
diff --git a/internal/scheduler/schedulerobjects/nodetype.go b/internal/scheduler/schedulerobjects/nodetype.go
index 2703dcc2..bc14d91c 100644
--- a/internal/scheduler/schedulerobjects/nodetype.go
+++ b/internal/scheduler/schedulerobjects/nodetype.go
@@ -1,8 +1,7 @@
 package schedulerobjects
 
 import (
-	"strings"
-
+	"github.com/segmentio/fasthash/fnv1a"
 	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
 	v1 "k8s.io/api/core/v1"
@@ -38,7 +37,7 @@ func NewNodeType(taints []v1.Taint, labels map[string]string, indexedTaints map[
 
 	// Sort taints to ensure node type id is consistent regardless of
 	// the order in which taints are set on the node.
-	slices.SortFunc(taints, func(a, b v1.Taint) bool { return a.Key < b.Key })
+	slices.SortFunc(taints, func(a, b v1.Taint) bool { return a.Key < b.Key }) // TODO: Use less ambiguous sorting.
 
 	// Filter out any labels that should not be indexed.
 	if indexedLabels != nil {
@@ -67,40 +66,44 @@ func NewNodeType(taints []v1.Taint, labels map[string]string, indexedTaints map[
 	}
 }
 
-// nodeTypeIdFromTaintsAndLabels generates an id that is unique for each combination
-// of taints, labels, and unset labels, of the form
-// $taint1$taint2...&$label1=labelValue1$label2=labelValue2...&$unsetIndexedLabel1=unsetIndexedLabelValue1...
+// nodeTypeIdFromTaintsAndLabels generates a id unique for each combination of taints, labels, and unset labels.
+// The id is based on the fnv1a hash. Hash collisions do not affect correctness, only the efficiency of sorting out nodes.
 //
 // We separate taints/labels by $, labels and values by =, and and groups by &,
 // since these characters are not allowed in taints and labels; see
 // https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set
 // https://man.archlinux.org/man/community/kubectl/kubectl-taint.1.en
-func nodeTypeIdFromTaintsAndLabels(taints []v1.Taint, labels, unsetIndexedLabels map[string]string) string {
-	// TODO: To reduce key size (and thus improve performance), we could hash the string.
+func nodeTypeIdFromTaintsAndLabels(taints []v1.Taint, labels, unsetIndexedLabels map[string]string) uint64 {
 	// TODO: We should test this function to ensure there are no collisions. And that the string is never empty.
-	var sb strings.Builder
+	h := fnv1a.Init64
 	for _, taint := range taints {
-		sb.WriteString("$")
-		sb.WriteString(taint.String())
+		h = fnv1a.AddString64(h, taint.Key)
+		h = fnv1a.AddString64(h, "=")
+		h = fnv1a.AddString64(h, taint.Value)
+		h = fnv1a.AddString64(h, ":")
+		h = fnv1a.AddString64(h, string(taint.Effect))
+		h = fnv1a.AddString64(h, "$")
 	}
-	sb.WriteString("&")
+	h = fnv1a.AddString64(h, "&")
+
 	ls := maps.Keys(labels)
 	slices.Sort(ls)
 	for _, label := range ls {
 		value := labels[label]
-		sb.WriteString("$")
-		sb.WriteString(label)
-		sb.WriteString("=")
-		sb.WriteString(value)
+		h = fnv1a.AddString64(h, label)
+		h = fnv1a.AddString64(h, "=")
+		h = fnv1a.AddString64(h, value)
+		h = fnv1a.AddString64(h, "$")
 	}
-	sb.WriteString("&")
+	h = fnv1a.AddString64(h, "&")
+
 	ls = maps.Keys(unsetIndexedLabels)
 	slices.Sort(ls)
 	for _, label := range ls {
-		sb.WriteString("$")
-		sb.WriteString(label)
+		h = fnv1a.AddString64(h, label)
+		h = fnv1a.AddString64(h, "$")
 	}
-	return sb.String()
+	return h
 }
 
 // getFilteredTaints returns a list of taints satisfying the filter predicate.
diff --git a/internal/scheduler/schedulerobjects/quantity.go b/internal/scheduler/schedulerobjects/quantity.go
deleted file mode 100644
index 769a7498..00000000
--- a/internal/scheduler/schedulerobjects/quantity.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package schedulerobjects
-
-import (
-	"encoding/binary"
-	"math"
-
-	"k8s.io/apimachinery/pkg/api/resource"
-)
-
-// EncodeQuantity returns the canonical byte representation of a Kubernetes resource.Quantity{}
-// used throughout the scheduler.
-func EncodeQuantity(val resource.Quantity) []byte {
-	// We assume that any quantity we want to compare can be represented as an int64.
-	return encodeInt(val.MilliValue())
-}
-
-func encodeInt(val int64) []byte {
-	size := 8
-	buf := make([]byte, size)
-
-	// This bit flips the sign bit on any sized signed twos-complement integer,
-	// which when truncated to a uint of the same size will bias the value such
-	// that the maximum negative int becomes 0, and the maximum positive int
-	// becomes the maximum positive uint.
-	scaled := val ^ int64(-1<<(size*8-1))
-
-	binary.BigEndian.PutUint64(buf, uint64(scaled))
-	return buf
-}
-
-// ScaleQuantity scales q in-place by a factor f.
-// This functions overflows for quantities the milli value of which can't be expressed as an int64.
-// E.g., 1Pi is ok, but not 10Pi.
-func ScaleQuantity(q resource.Quantity, f float64) resource.Quantity {
-	q.SetMilli(int64(math.Round(float64(q.MilliValue()) * f)))
-	return q
-}
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
index 7baa963d..ad9614de 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.pb.go
@@ -161,6 +161,8 @@ type Node struct {
 	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
 	// Kubernetes node name. Not guaranteed to be unique across clusters.
 	Name string `protobuf:"bytes,12,opt,name=name,proto3" json:"name,omitempty"`
+	// Keys by which nodes are sorted in the nodeDb.
+	NodeDbKeys [][]byte `protobuf:"bytes,18,rep,name=nodeDbKeys,proto3" json:"nodeDbKeys,omitempty"`
 	// Time at which this node was last updated.
 	// Used to garbage collect nodes that have been removed.
 	LastSeen time.Time `protobuf:"bytes,2,opt,name=last_seen,json=lastSeen,proto3,stdtime" json:"lastSeen"`
@@ -170,7 +172,7 @@ type Node struct {
 	NodeType *NodeType `protobuf:"bytes,3,opt,name=node_type,json=nodeType,proto3" json:"nodeType,omitempty"`
 	// We store NodeType.id here to simplify indexing.
 	// This field is set automatically by the NodeDb on inserting the node.
-	NodeTypeId string `protobuf:"bytes,4,opt,name=node_type_id,json=nodeTypeId,proto3" json:"nodeTypeId,omitempty"`
+	NodeTypeId uint64 `protobuf:"varint,4,opt,name=node_type_id,json=nodeTypeId,proto3" json:"nodeTypeId,omitempty"`
 	// Kubernetes taints.
 	Taints []v1.Taint `protobuf:"bytes,5,rep,name=taints,proto3" json:"taints"`
 	// Kubernetes labels.
@@ -248,6 +250,13 @@ func (m *Node) GetName() string {
 	return ""
 }
 
+func (m *Node) GetNodeDbKeys() [][]byte {
+	if m != nil {
+		return m.NodeDbKeys
+	}
+	return nil
+}
+
 func (m *Node) GetLastSeen() time.Time {
 	if m != nil {
 		return m.LastSeen
@@ -262,11 +271,11 @@ func (m *Node) GetNodeType() *NodeType {
 	return nil
 }
 
-func (m *Node) GetNodeTypeId() string {
+func (m *Node) GetNodeTypeId() uint64 {
 	if m != nil {
 		return m.NodeTypeId
 	}
-	return ""
+	return 0
 }
 
 func (m *Node) GetTaints() []v1.Taint {
@@ -358,7 +367,7 @@ func (m *Node) GetReportingNodeType() string {
 // the scheduler only considers nodes with a NodeType for which the taints and labels match.
 type NodeType struct {
 	// Unique identifier. Used for map lookup.
-	Id string `protobuf:"bytes,1,opt,name=Id,proto3" json:"Id,omitempty"`
+	Id uint64 `protobuf:"varint,1,opt,name=Id,proto3" json:"Id,omitempty"`
 	// Kubernetes taints.
 	// To reduce the number of distinct node types,
 	// may contain only a subset of the taints of the node the node type is created from.
@@ -406,11 +415,11 @@ func (m *NodeType) XXX_DiscardUnknown() {
 
 var xxx_messageInfo_NodeType proto.InternalMessageInfo
 
-func (m *NodeType) GetId() string {
+func (m *NodeType) GetId() uint64 {
 	if m != nil {
 		return m.Id
 	}
-	return ""
+	return 0
 }
 
 func (m *NodeType) GetTaints() []v1.Taint {
@@ -998,139 +1007,141 @@ func init() {
 }
 
 var fileDescriptor_97dadc5fbd620721 = []byte{
-	// 2104 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x19, 0x4d, 0x6f, 0xe3, 0xc6,
-	0xd5, 0xb4, 0xfc, 0x21, 0x3d, 0xf9, 0x43, 0x1e, 0xdb, 0xbb, 0xb4, 0x36, 0x2b, 0x3a, 0x4a, 0x1a,
-	0x38, 0xcd, 0x46, 0x6a, 0x9c, 0x02, 0x5d, 0xb8, 0xbd, 0x58, 0xb6, 0xdb, 0x95, 0xb3, 0x91, 0xbd,
-	0x94, 0xdd, 0xa2, 0x05, 0x1a, 0x82, 0x12, 0xc7, 0x5a, 0xc6, 0xd4, 0x8c, 0x96, 0x1c, 0xba, 0x51,
-	0xce, 0xed, 0xa1, 0x08, 0x90, 0x06, 0x6d, 0x50, 0x04, 0x28, 0xd0, 0x22, 0xb7, 0xfe, 0x82, 0x1e,
-	0xda, 0x5b, 0x4f, 0x39, 0xe6, 0xd8, 0x93, 0x5a, 0xec, 0xde, 0x74, 0xec, 0x2f, 0x28, 0x38, 0x24,
-	0xc5, 0x11, 0x49, 0x59, 0xde, 0xb4, 0xdb, 0x3d, 0x59, 0xf3, 0xbe, 0xbf, 0xe6, 0xbd, 0xc7, 0x31,
-	0xec, 0x99, 0x84, 0x61, 0x9b, 0xe8, 0x56, 0xd5, 0x69, 0x3f, 0xc6, 0x86, 0x6b, 0x61, 0x3b, 0xfa,
-	0x45, 0x5b, 0x1f, 0xe2, 0x36, 0x73, 0x12, 0x80, 0x4a, 0xcf, 0xa6, 0x8c, 0xa2, 0x42, 0x1c, 0x5e,
-	0x54, 0x3a, 0x94, 0x76, 0x2c, 0x5c, 0xe5, 0xf8, 0x96, 0x7b, 0x51, 0x65, 0x66, 0x17, 0x3b, 0x4c,
-	0xef, 0xf6, 0x7c, 0x96, 0x62, 0xf9, 0xf2, 0xbe, 0x53, 0x31, 0x69, 0x55, 0xef, 0x99, 0xd5, 0x36,
-	0xb5, 0x71, 0xf5, 0xea, 0x9d, 0x6a, 0x07, 0x13, 0x6c, 0xeb, 0x0c, 0x1b, 0x01, 0xcd, 0x77, 0x23,
-	0x9a, 0xae, 0xde, 0x7e, 0x6c, 0x12, 0x6c, 0xf7, 0xab, 0xbd, 0xcb, 0x0e, 0x67, 0xb2, 0xb1, 0x43,
-	0x5d, 0xbb, 0x8d, 0x13, 0x5c, 0x6f, 0x77, 0x4c, 0xf6, 0xd8, 0x6d, 0x55, 0xda, 0xb4, 0x5b, 0xed,
-	0xd0, 0x0e, 0x8d, 0x6c, 0xf0, 0x4e, 0xfc, 0xc0, 0x7f, 0xf9, 0xe4, 0xe5, 0x3f, 0x67, 0x20, 0x7b,
-	0xf4, 0x11, 0x6e, 0xbb, 0x8c, 0xda, 0x68, 0x1b, 0x66, 0x4d, 0x43, 0x96, 0xb6, 0xa5, 0x9d, 0x5c,
-	0xad, 0x30, 0x1c, 0x28, 0x4b, 0xa6, 0x71, 0x8f, 0x76, 0x4d, 0x86, 0xbb, 0x3d, 0xd6, 0x57, 0x67,
-	0x4d, 0x03, 0xbd, 0x01, 0x73, 0x3d, 0x4a, 0x2d, 0x79, 0x96, 0xd3, 0xa0, 0xe1, 0x40, 0x59, 0xf1,
-	0xce, 0x02, 0x15, 0xc7, 0xa3, 0x7d, 0x98, 0x27, 0xd4, 0xc0, 0x8e, 0x9c, 0xd9, 0xce, 0xec, 0xe4,
-	0x77, 0x6f, 0x55, 0x12, 0xa1, 0x6b, 0x50, 0x03, 0xd7, 0xd6, 0x87, 0x03, 0x65, 0x95, 0x13, 0x0a,
-	0x12, 0x7c, 0x4e, 0xf4, 0x01, 0xac, 0x74, 0x4d, 0x62, 0x76, 0xdd, 0xee, 0x31, 0x6d, 0x35, 0xcd,
-	0x8f, 0xb1, 0x3c, 0xb7, 0x2d, 0xed, 0xe4, 0x77, 0x4b, 0x49, 0x59, 0x6a, 0x10, 0x8c, 0x87, 0xa6,
-	0xc3, 0x6a, 0xb7, 0xbe, 0x1a, 0x28, 0x33, 0x9e, 0x61, 0xe3, 0xdc, 0x6a, 0xec, 0xec, 0xc9, 0xb7,
-	0x74, 0x87, 0x9d, 0xf7, 0x0c, 0x9d, 0xe1, 0x33, 0xb3, 0x8b, 0xe5, 0x79, 0x2e, 0xbf, 0x58, 0xf1,
-	0x93, 0x57, 0x09, 0x03, 0x57, 0x39, 0x0b, 0x93, 0x57, 0x2b, 0x86, 0xb2, 0xc7, 0x39, 0x3f, 0xfb,
-	0xa7, 0x22, 0xa9, 0x31, 0x18, 0x3a, 0x81, 0x75, 0x97, 0xe8, 0x8e, 0x63, 0x76, 0x08, 0x36, 0xb4,
-	0x0f, 0x69, 0x4b, 0xb3, 0x5d, 0xe2, 0xc8, 0xb9, 0xed, 0xcc, 0x4e, 0xae, 0xa6, 0x0c, 0x07, 0xca,
-	0x9d, 0x08, 0x7d, 0x4c, 0x5b, 0xaa, 0x4b, 0xc4, 0x20, 0xac, 0x25, 0x90, 0xe5, 0xbf, 0x6e, 0xc2,
-	0x9c, 0x17, 0xb5, 0x9b, 0xa5, 0x89, 0xe8, 0x5d, 0x2c, 0x2f, 0x45, 0x69, 0xf2, 0xce, 0x62, 0x9a,
-	0xbc, 0x33, 0x7a, 0x1f, 0x72, 0x9e, 0xd5, 0x9a, 0x83, 0x31, 0xe1, 0x39, 0xbd, 0xde, 0xfd, 0x8d,
-	0xc0, 0xfd, 0xac, 0xc7, 0xd4, 0xc4, 0x98, 0x70, 0xc7, 0x47, 0x27, 0x74, 0x02, 0x39, 0x2f, 0x77,
-	0x1a, 0xeb, 0xf7, 0xb0, 0x9c, 0x09, 0xc4, 0xa5, 0x66, 0xfe, 0xac, 0xdf, 0xc3, 0xb5, 0x5b, 0xc3,
-	0x81, 0x82, 0x48, 0x70, 0x12, 0x6c, 0xcb, 0x86, 0x30, 0xb4, 0x07, 0x4b, 0x23, 0x81, 0x9a, 0x69,
-	0xf0, 0x0a, 0xc8, 0xd5, 0xe4, 0xe1, 0x40, 0xd9, 0x08, 0x69, 0xea, 0xa2, 0xef, 0x10, 0x41, 0xd1,
-	0x3e, 0x2c, 0x30, 0xdd, 0x24, 0xcc, 0x91, 0xe7, 0x79, 0x0d, 0x6e, 0x55, 0xfc, 0xfb, 0x54, 0xd1,
-	0x7b, 0x66, 0xc5, 0xbb, 0x73, 0x95, 0xab, 0x77, 0x2a, 0x67, 0x1e, 0x45, 0x6d, 0x25, 0xf0, 0x2b,
-	0x60, 0x50, 0x83, 0xbf, 0xe8, 0x14, 0x16, 0x2c, 0xbd, 0x85, 0x2d, 0x47, 0x5e, 0xe0, 0x22, 0xca,
-	0xe9, 0xce, 0x54, 0x1e, 0x72, 0xa2, 0x23, 0xc2, 0xec, 0x7e, 0x6d, 0x63, 0x38, 0x50, 0x0a, 0x3e,
-	0x97, 0x60, 0x58, 0x20, 0x07, 0x69, 0xb0, 0xca, 0x28, 0xd3, 0x2d, 0x2d, 0xbc, 0xbf, 0x8e, 0xbc,
-	0xf8, 0x7c, 0x55, 0xcd, 0xd9, 0x43, 0x94, 0xa3, 0xc6, 0xce, 0xe8, 0x2f, 0x12, 0xbc, 0xae, 0x5b,
-	0x16, 0x6d, 0xeb, 0x4c, 0x6f, 0x59, 0x58, 0x6b, 0xf5, 0xb5, 0x9e, 0x6d, 0x52, 0xdb, 0x64, 0x7d,
-	0x4d, 0x27, 0xc6, 0x48, 0xaf, 0x9c, 0xe5, 0x1e, 0xfd, 0x60, 0x82, 0x47, 0xfb, 0x91, 0x88, 0x5a,
-	0xff, 0x34, 0x10, 0xb0, 0x4f, 0x8c, 0x50, 0x91, 0xef, 0xeb, 0x4e, 0x60, 0xd4, 0xb6, 0x3e, 0x85,
-	0x5c, 0x9d, 0x4a, 0x81, 0x6c, 0x58, 0x77, 0x98, 0xce, 0xb8, 0xc5, 0xc1, 0x65, 0xf1, 0x32, 0x9e,
-	0xe3, 0x66, 0xbe, 0x35, 0xc1, 0xcc, 0xa6, 0xc7, 0x51, 0xeb, 0xfb, 0x37, 0xa4, 0x6e, 0xf8, 0x56,
-	0xdd, 0x0e, 0xac, 0x5a, 0x75, 0xc6, 0xb1, 0x6a, 0x1c, 0x80, 0x5c, 0x58, 0x0f, 0xec, 0xc2, 0x46,
-	0xa8, 0xd7, 0x34, 0x64, 0xe0, 0x3a, 0xef, 0x5d, 0x1f, 0x1a, 0x6c, 0x70, 0x41, 0xa1, 0x52, 0x39,
-	0x50, 0x5a, 0xd0, 0x63, 0x68, 0x35, 0x01, 0x41, 0x0c, 0xd0, 0x98, 0xda, 0x27, 0x2e, 0x76, 0xb1,
-	0x9c, 0xbf, 0xa9, 0xd6, 0x47, 0x1e, 0xf9, 0x64, 0xad, 0x1c, 0xad, 0x26, 0x20, 0x9e, 0xb3, 0xf8,
-	0xca, 0x6c, 0xb3, 0xa8, 0x19, 0x69, 0xa6, 0xe1, 0xc8, 0x2b, 0xd7, 0xaa, 0x3d, 0xf2, 0x39, 0xc2,
-	0x88, 0x39, 0x31, 0xb5, 0x38, 0x86, 0x56, 0x13, 0x10, 0xf4, 0xa5, 0x04, 0x25, 0x42, 0x89, 0xa6,
-	0xdb, 0x5d, 0xdd, 0xd0, 0xb5, 0xc8, 0xf1, 0xe8, 0x06, 0x2c, 0x73, 0x13, 0xbe, 0x37, 0xc1, 0x84,
-	0x06, 0x25, 0xfb, 0x9c, 0x77, 0x14, 0x82, 0x51, 0xb5, 0xfb, 0xd6, 0xbc, 0x16, 0x58, 0x73, 0x87,
-	0x4c, 0xa6, 0x54, 0xaf, 0x43, 0xa2, 0x7d, 0x58, 0x76, 0x49, 0xa0, 0xdd, 0xab, 0x50, 0x79, 0x75,
-	0x5b, 0xda, 0xc9, 0xd6, 0xee, 0x0c, 0x07, 0xca, 0xed, 0x31, 0x84, 0x70, 0xa3, 0xc7, 0x39, 0xd0,
-	0x27, 0x12, 0xdc, 0x0e, 0x3d, 0xd2, 0x5c, 0x47, 0xef, 0xe0, 0x28, 0xb3, 0x05, 0xee, 0xdf, 0x77,
-	0x26, 0xf8, 0x17, 0x9a, 0x71, 0xee, 0x31, 0x8d, 0x65, 0xb7, 0x3c, 0x1c, 0x28, 0x25, 0x3b, 0x05,
-	0x2d, 0x98, 0xb1, 0x91, 0x86, 0xf7, 0x66, 0x8f, 0x8d, 0x7b, 0xd4, 0x66, 0x26, 0xe9, 0x68, 0x51,
-	0x4b, 0x5e, 0xe3, 0xed, 0x93, 0xcf, 0x9e, 0x11, 0xba, 0x91, 0xec, 0xbf, 0x6b, 0x09, 0x64, 0x51,
-	0x87, 0xbc, 0xd0, 0xe4, 0xd0, 0x6b, 0x90, 0xb9, 0xc4, 0xfd, 0x60, 0x04, 0xad, 0x0d, 0x07, 0xca,
-	0xf2, 0x25, 0xee, 0x0b, 0x12, 0x3c, 0x2c, 0x7a, 0x13, 0xe6, 0xaf, 0x74, 0xcb, 0xc5, 0xc1, 0xb2,
-	0xc0, 0x67, 0x3d, 0x07, 0x88, 0xb3, 0x9e, 0x03, 0xf6, 0x66, 0xef, 0x4b, 0xc5, 0x3f, 0x48, 0xf0,
-	0xad, 0x1b, 0xb5, 0x1d, 0x51, 0xfb, 0xfc, 0x44, 0xed, 0x75, 0x51, 0xfb, 0xf4, 0xfe, 0x3a, 0xcd,
-	0xba, 0x5f, 0x4b, 0xb0, 0x91, 0xd6, 0x6d, 0x6e, 0x16, 0x8a, 0x07, 0xa2, 0x31, 0x2b, 0xbb, 0x77,
-	0x93, 0xc6, 0xf8, 0x42, 0x7d, 0x0d, 0xd3, 0x6c, 0xf9, 0x44, 0x82, 0xcd, 0xd4, 0x2e, 0x74, 0x33,
-	0x63, 0xfe, 0xc7, 0x91, 0x89, 0x59, 0x13, 0xd5, 0xef, 0x4b, 0xb1, 0xe6, 0x12, 0x36, 0x53, 0x7b,
-	0xd6, 0x37, 0x28, 0xd9, 0xec, 0x54, 0x65, 0xbf, 0x97, 0x60, 0x7b, 0x5a, 0x7b, 0x7a, 0x29, 0xd5,
-	0xfa, 0x1b, 0x09, 0xb6, 0x26, 0xf6, 0x95, 0x97, 0x91, 0x97, 0xf2, 0x1f, 0xe7, 0x20, 0x1b, 0x76,
-	0x13, 0x6f, 0x81, 0xad, 0x8f, 0x2d, 0xb0, 0x63, 0x4b, 0xdc, 0xec, 0xd8, 0xf2, 0x36, 0xfb, 0x4d,
-	0x97, 0xb7, 0xb3, 0xd1, 0xf2, 0xe6, 0x7f, 0x83, 0xbc, 0x31, 0x79, 0x13, 0x7d, 0x8e, 0x05, 0xee,
-	0x97, 0x12, 0x20, 0x97, 0x38, 0x98, 0xd5, 0x89, 0x81, 0x3f, 0xc2, 0x86, 0xcf, 0x29, 0xcf, 0x71,
-	0x15, 0xbb, 0xd7, 0xa8, 0x38, 0x4f, 0x30, 0xf9, 0xea, 0xb6, 0x87, 0x03, 0xe5, 0x95, 0xa4, 0x44,
-	0x41, 0x75, 0x8a, 0xbe, 0xff, 0x47, 0x3f, 0xee, 0xc2, 0xed, 0x09, 0x36, 0xbf, 0x08, 0x75, 0xe5,
-	0x7f, 0x67, 0x60, 0x8b, 0xd7, 0xe8, 0x81, 0xe5, 0x3a, 0x0c, 0xdb, 0x63, 0xe5, 0x8b, 0xea, 0xb0,
-	0xd8, 0xb6, 0xb1, 0x77, 0xbb, 0xb8, 0xd6, 0xeb, 0x3f, 0x53, 0xd6, 0x83, 0x8a, 0x08, 0x59, 0xf8,
-	0x57, 0x4a, 0x78, 0xf0, 0xec, 0xf2, 0xc7, 0xb2, 0x60, 0xd7, 0x93, 0xd8, 0x54, 0xf5, 0x29, 0xd0,
-	0x7d, 0x00, 0x1c, 0x7c, 0x1b, 0xd7, 0x0d, 0xfe, 0x41, 0x13, 0x7c, 0x7c, 0x44, 0x50, 0xf1, 0xe3,
-	0x23, 0x82, 0xa2, 0xdf, 0x49, 0xde, 0x04, 0x0e, 0xfa, 0x40, 0x34, 0xca, 0x82, 0x3a, 0x39, 0x4c,
-	0xd6, 0xc9, 0x44, 0xd7, 0x47, 0xd7, 0x4c, 0x10, 0xe3, 0x57, 0xce, 0x9d, 0xc0, 0xcd, 0x34, 0x45,
-	0x6a, 0x1a, 0xb0, 0xf8, 0xa9, 0x04, 0xf2, 0x24, 0x71, 0x2f, 0xa3, 0x4f, 0x95, 0xff, 0x96, 0x81,
-	0x62, 0x9a, 0xd3, 0x2a, 0x5f, 0x40, 0x46, 0xaf, 0x0d, 0xd2, 0x94, 0xd7, 0x06, 0xa1, 0x3a, 0x66,
-	0xff, 0xcb, 0xea, 0xf8, 0x54, 0x82, 0x82, 0x10, 0x3a, 0x9e, 0x96, 0xa0, 0x81, 0xd4, 0x92, 0xce,
-	0x4e, 0xb6, 0x5d, 0x4c, 0x9b, 0xb0, 0xd2, 0x95, 0x86, 0x03, 0xa5, 0x18, 0x97, 0x2f, 0xf8, 0x93,
-	0xd0, 0x5d, 0xfc, 0x42, 0x82, 0xcd, 0x54, 0x59, 0x37, 0xbb, 0x85, 0x3f, 0x1e, 0x4f, 0xd8, 0x5b,
-	0xcf, 0x51, 0x79, 0x53, 0xb3, 0xf7, 0xab, 0x59, 0x58, 0x12, 0xd3, 0x8d, 0x3e, 0x80, 0x5c, 0xb4,
-	0xd5, 0x4b, 0x3c, 0x68, 0x6f, 0x5f, 0x5f, 0x21, 0x95, 0xd8, 0x2e, 0xbf, 0x16, 0x24, 0x27, 0x92,
-	0xa3, 0x46, 0x3f, 0x8b, 0x9f, 0x4b, 0xb0, 0x32, 0x79, 0xba, 0x4e, 0x0e, 0xc2, 0x4f, 0xc7, 0x83,
-	0x50, 0x11, 0x86, 0xc9, 0xe8, 0x65, 0xad, 0xd2, 0xbb, 0xec, 0xf0, 0xe9, 0x12, 0xaa, 0xab, 0x3c,
-	0x72, 0x75, 0xc2, 0x4c, 0xd6, 0x9f, 0x1a, 0x87, 0xcf, 0xe7, 0x61, 0xed, 0x98, 0xb6, 0x9a, 0xbe,
-	0xa3, 0x26, 0xe9, 0xd4, 0xc9, 0x05, 0x45, 0xbb, 0x90, 0xb5, 0xcc, 0x0b, 0xcc, 0xcc, 0x2e, 0xe6,
-	0xe6, 0x2d, 0xfb, 0xef, 0x1d, 0x21, 0x4c, 0x7c, 0xef, 0x08, 0x61, 0x68, 0x0f, 0x96, 0x74, 0xa6,
-	0x75, 0xa9, 0xc3, 0x34, 0x4a, 0xda, 0xe1, 0x1a, 0xc2, 0x5b, 0x8e, 0xce, 0xde, 0xa7, 0x0e, 0x3b,
-	0x21, 0x6d, 0x91, 0x13, 0x22, 0x28, 0xfa, 0x3e, 0xe4, 0x7b, 0x36, 0xf6, 0xe0, 0xa6, 0xf7, 0x09,
-	0x93, 0xe1, 0xac, 0x5b, 0xc3, 0x81, 0xb2, 0x29, 0x80, 0x05, 0x5e, 0x91, 0x1a, 0x3d, 0x80, 0x42,
-	0x9b, 0x92, 0xb6, 0x6b, 0xdb, 0x98, 0xb4, 0xfb, 0x9a, 0xa3, 0x5f, 0xf8, 0xcf, 0x6d, 0xd9, 0xda,
-	0xdd, 0xe1, 0x40, 0xd9, 0x12, 0x70, 0x4d, 0xfd, 0x42, 0x94, 0xb2, 0x1a, 0x43, 0x79, 0x9f, 0x1e,
-	0xa3, 0x07, 0x87, 0xb6, 0xa5, 0x3b, 0x8e, 0xc6, 0x5f, 0xa2, 0x16, 0xa2, 0x4f, 0x8f, 0x10, 0x7d,
-	0xe0, 0x61, 0x1b, 0xe3, 0xcf, 0x52, 0x6b, 0x09, 0x24, 0x6a, 0x42, 0xde, 0x71, 0x5b, 0x5d, 0x93,
-	0x69, 0x3c, 0x94, 0x8b, 0x53, 0x2f, 0x78, 0xf8, 0x54, 0x02, 0x3e, 0xdb, 0xe8, 0x81, 0x4e, 0x38,
-	0x7b, 0xc9, 0x09, 0x35, 0xc9, 0xd9, 0x28, 0x39, 0x21, 0x4c, 0x4c, 0x4e, 0x08, 0x43, 0xbf, 0x80,
-	0x75, 0xbf, 0x84, 0x35, 0x1b, 0x3f, 0x71, 0x4d, 0x1b, 0x77, 0x71, 0xf4, 0xba, 0xf4, 0x7a, 0xb2,
-	0xce, 0x4f, 0xf8, 0x5f, 0x55, 0xa0, 0xf5, 0x87, 0x3d, 0x4d, 0xc0, 0xc5, 0x61, 0x9f, 0xc4, 0xa2,
-	0x2a, 0x2c, 0x5e, 0x61, 0xdb, 0x31, 0x29, 0x91, 0x73, 0xdc, 0xd6, 0xcd, 0xe1, 0x40, 0x59, 0x0b,
-	0x40, 0x02, 0x6f, 0x48, 0xb5, 0x37, 0xf7, 0xc5, 0x97, 0x8a, 0x54, 0xfe, 0xad, 0x04, 0x28, 0x69,
-	0x03, 0xb2, 0x60, 0xb5, 0x47, 0x0d, 0x11, 0x14, 0x8c, 0xd4, 0x57, 0x93, 0x2e, 0x9c, 0x8e, 0x13,
-	0xfa, 0xc5, 0x10, 0xe3, 0x8e, 0x0c, 0x78, 0x30, 0xa3, 0xc6, 0x45, 0xd7, 0x56, 0x60, 0x49, 0x8c,
-	0x56, 0xf9, 0xef, 0x8b, 0xb0, 0x1a, 0x93, 0x8a, 0x1c, 0xff, 0x95, 0xaf, 0x89, 0x2d, 0xdc, 0x66,
-	0xd4, 0x0e, 0x3a, 0xc7, 0xbb, 0x53, 0xcd, 0xe1, 0xcb, 0x55, 0xc8, 0xe5, 0xf7, 0x8f, 0xe2, 0x70,
-	0xa0, 0xdc, 0x12, 0x85, 0x09, 0xe1, 0x19, 0x53, 0x82, 0x4e, 0x21, 0xab, 0x5f, 0x5c, 0x98, 0xc4,
-	0xab, 0x00, 0xbf, 0x2d, 0xbc, 0x92, 0xb6, 0x63, 0xee, 0x07, 0x34, 0x7e, 0x7d, 0x84, 0x1c, 0x62,
-	0x7d, 0x84, 0x30, 0x74, 0x0e, 0x79, 0x46, 0x2d, 0x6c, 0xeb, 0xcc, 0xa4, 0x24, 0xdc, 0x3a, 0x4b,
-	0xa9, 0x8b, 0xeb, 0x88, 0x6c, 0x34, 0x8d, 0x44, 0x56, 0x55, 0x3c, 0x20, 0x0a, 0x79, 0x9d, 0x10,
-	0xca, 0x02, 0xb1, 0x8b, 0x93, 0x36, 0xcd, 0x78, 0x70, 0xf6, 0x23, 0x26, 0x3f, 0x36, 0xbc, 0x17,
-	0x08, 0xa2, 0xc4, 0x5e, 0x20, 0x80, 0xc7, 0xee, 0xc6, 0x1c, 0xdf, 0x06, 0xa6, 0xdf, 0x8d, 0x63,
-	0x28, 0x84, 0xed, 0x84, 0x92, 0x53, 0x6a, 0x99, 0xed, 0x3e, 0x7f, 0x4e, 0xcf, 0xf9, 0x13, 0x2f,
-	0x8e, 0x13, 0x27, 0x5e, 0x1c, 0x87, 0x3e, 0x86, 0xd1, 0xa3, 0xc6, 0x58, 0x95, 0x2e, 0xf0, 0x2c,
-	0xed, 0xa4, 0x05, 0x54, 0x4d, 0xa1, 0xaf, 0xbd, 0x12, 0x84, 0x36, 0x55, 0x9a, 0x9a, 0x0a, 0x45,
-	0x4d, 0x58, 0x6f, 0xeb, 0x5e, 0x64, 0xa3, 0x66, 0xfe, 0x1e, 0xf6, 0x5b, 0xc4, 0x52, 0xed, 0xd5,
-	0xe1, 0x40, 0xb9, 0x9b, 0x82, 0x16, 0xbc, 0x49, 0xe3, 0x2e, 0x76, 0x60, 0x2d, 0x51, 0xa9, 0x2f,
-	0x64, 0x65, 0xbf, 0x80, 0x42, 0x3c, 0xeb, 0x2f, 0x64, 0x57, 0xff, 0x93, 0x04, 0x5b, 0xa7, 0xae,
-	0xe5, 0xe8, 0x76, 0x33, 0xac, 0xc2, 0x63, 0xda, 0x3a, 0xc4, 0x4c, 0x37, 0x2d, 0xc7, 0x13, 0xc6,
-	0x9f, 0x24, 0x02, 0x9d, 0x5c, 0x18, 0x07, 0x88, 0xc2, 0xfc, 0x97, 0xd0, 0x37, 0x61, 0xfe, 0x51,
-	0x7c, 0x17, 0x8f, 0xaf, 0x44, 0x3e, 0x05, 0xba, 0x07, 0x0b, 0xde, 0x8c, 0xc5, 0x2c, 0xd8, 0xc3,
-	0xf9, 0x67, 0x9a, 0x0f, 0x11, 0x3f, 0xd3, 0x7c, 0xc8, 0xb7, 0x4f, 0x20, 0x2f, 0xbc, 0xa8, 0xa0,
-	0x3c, 0x2c, 0x9e, 0x37, 0xde, 0x6b, 0x9c, 0xfc, 0xa4, 0x51, 0x98, 0xf1, 0x0e, 0xa7, 0x47, 0x8d,
-	0xc3, 0x7a, 0xe3, 0x47, 0x05, 0xc9, 0x3b, 0xa8, 0xe7, 0x8d, 0x86, 0x77, 0x98, 0x45, 0xcb, 0x90,
-	0x6b, 0x9e, 0x1f, 0x1c, 0x1c, 0x1d, 0x1d, 0x1e, 0x1d, 0x16, 0x32, 0x08, 0x60, 0xe1, 0x87, 0xfb,
-	0xf5, 0x87, 0x47, 0x87, 0x85, 0xb9, 0xda, 0xcf, 0xbf, 0x7a, 0x5a, 0x92, 0xbe, 0x7e, 0x5a, 0x92,
-	0xfe, 0xf5, 0xb4, 0x24, 0x7d, 0xf6, 0xac, 0x34, 0xf3, 0xf5, 0xb3, 0xd2, 0xcc, 0x3f, 0x9e, 0x95,
-	0x66, 0x7e, 0x76, 0x20, 0xfc, 0xc3, 0xcd, 0x7f, 0xe4, 0xec, 0xd9, 0xd4, 0xbb, 0x92, 0xc1, 0xa9,
-	0x7a, 0x83, 0xff, 0x2c, 0xb6, 0x16, 0xf8, 0x1c, 0x7b, 0xf7, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff,
-	0x91, 0xc9, 0x86, 0xa3, 0x87, 0x1c, 0x00, 0x00,
+	// 2132 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x19, 0x4d, 0x6f, 0x1b, 0xc7,
+	0x55, 0x2b, 0x52, 0x12, 0x39, 0x94, 0x25, 0x6a, 0x64, 0xd9, 0x2b, 0xda, 0xe6, 0x32, 0x4c, 0x1a,
+	0x28, 0x8d, 0x43, 0x36, 0x4e, 0x81, 0x1a, 0x6e, 0x2f, 0xa2, 0xa5, 0xd6, 0x94, 0x1d, 0x4a, 0x5e,
+	0x4a, 0x2d, 0x5a, 0xa0, 0x59, 0x2c, 0xb9, 0x23, 0x7a, 0xa3, 0xe5, 0x0c, 0xbd, 0x3b, 0xab, 0x86,
+	0x39, 0xb7, 0x87, 0x22, 0x40, 0x1a, 0xb4, 0x41, 0x11, 0xa0, 0x40, 0x8b, 0xdc, 0xfa, 0x0b, 0x7a,
+	0xe9, 0xad, 0xa7, 0x1c, 0x73, 0xec, 0x89, 0x2d, 0xec, 0x1b, 0x8f, 0x45, 0x7f, 0x40, 0x31, 0x33,
+	0xbb, 0xdc, 0xe1, 0xee, 0x52, 0x94, 0xd3, 0xba, 0x3e, 0x71, 0xe7, 0x7d, 0x7f, 0xcd, 0x9b, 0x37,
+	0x43, 0x70, 0xcf, 0xc6, 0x14, 0xb9, 0xd8, 0x74, 0xea, 0x5e, 0xf7, 0x09, 0xb2, 0x7c, 0x07, 0xb9,
+	0xd1, 0x17, 0xe9, 0x7c, 0x88, 0xba, 0xd4, 0x4b, 0x00, 0x6a, 0x03, 0x97, 0x50, 0x02, 0x8b, 0x71,
+	0x78, 0x49, 0xeb, 0x11, 0xd2, 0x73, 0x50, 0x9d, 0xe3, 0x3b, 0xfe, 0x69, 0x9d, 0xda, 0x7d, 0xe4,
+	0x51, 0xb3, 0x3f, 0x10, 0x2c, 0xa5, 0xea, 0xd9, 0x5d, 0xaf, 0x66, 0x93, 0xba, 0x39, 0xb0, 0xeb,
+	0x5d, 0xe2, 0xa2, 0xfa, 0xf9, 0xbb, 0xf5, 0x1e, 0xc2, 0xc8, 0x35, 0x29, 0xb2, 0x02, 0x9a, 0xef,
+	0x46, 0x34, 0x7d, 0xb3, 0xfb, 0xc4, 0xc6, 0xc8, 0x1d, 0xd6, 0x07, 0x67, 0x3d, 0xce, 0xe4, 0x22,
+	0x8f, 0xf8, 0x6e, 0x17, 0x25, 0xb8, 0xde, 0xe9, 0xd9, 0xf4, 0x89, 0xdf, 0xa9, 0x75, 0x49, 0xbf,
+	0xde, 0x23, 0x3d, 0x12, 0xd9, 0xc0, 0x56, 0x7c, 0xc1, 0xbf, 0x04, 0x79, 0xf5, 0xcf, 0x19, 0x90,
+	0xdb, 0xff, 0x08, 0x75, 0x7d, 0x4a, 0x5c, 0x58, 0x01, 0x8b, 0xb6, 0xa5, 0x2a, 0x15, 0x65, 0x27,
+	0xdf, 0x28, 0x8e, 0x47, 0xda, 0xaa, 0x6d, 0xdd, 0x26, 0x7d, 0x9b, 0xa2, 0xfe, 0x80, 0x0e, 0xf5,
+	0x45, 0xdb, 0x82, 0x6f, 0x82, 0xec, 0x80, 0x10, 0x47, 0x5d, 0xe4, 0x34, 0x70, 0x3c, 0xd2, 0xd6,
+	0xd8, 0x5a, 0xa2, 0xe2, 0x78, 0xb8, 0x0b, 0x96, 0x30, 0xb1, 0x90, 0xa7, 0x66, 0x2a, 0x99, 0x9d,
+	0xc2, 0x9d, 0x6b, 0xb5, 0x44, 0xe8, 0x5a, 0xc4, 0x42, 0x8d, 0xcd, 0xf1, 0x48, 0x5b, 0xe7, 0x84,
+	0x92, 0x04, 0xc1, 0x09, 0x3f, 0x00, 0x6b, 0x7d, 0x1b, 0xdb, 0x7d, 0xbf, 0x7f, 0x40, 0x3a, 0x6d,
+	0xfb, 0x63, 0xa4, 0x66, 0x2b, 0xca, 0x4e, 0xe1, 0x4e, 0x39, 0x29, 0x4b, 0x0f, 0x82, 0xf1, 0xc8,
+	0xf6, 0x68, 0xe3, 0xda, 0x57, 0x23, 0x6d, 0x81, 0x19, 0x36, 0xcd, 0xad, 0xc7, 0xd6, 0x4c, 0xbe,
+	0x63, 0x7a, 0xf4, 0x64, 0x60, 0x99, 0x14, 0x1d, 0xdb, 0x7d, 0xa4, 0x2e, 0x71, 0xf9, 0xa5, 0x9a,
+	0x48, 0x5e, 0x2d, 0x0c, 0x5c, 0xed, 0x38, 0x4c, 0x5e, 0xa3, 0x14, 0xca, 0x9e, 0xe6, 0xfc, 0xec,
+	0x1f, 0x9a, 0xa2, 0xc7, 0x60, 0xf0, 0x10, 0x6c, 0xfa, 0xd8, 0xf4, 0x3c, 0xbb, 0x87, 0x91, 0x65,
+	0x7c, 0x48, 0x3a, 0x86, 0xeb, 0x63, 0x4f, 0xcd, 0x57, 0x32, 0x3b, 0xf9, 0x86, 0x36, 0x1e, 0x69,
+	0x37, 0x22, 0xf4, 0x01, 0xe9, 0xe8, 0x3e, 0x96, 0x83, 0xb0, 0x91, 0x40, 0x56, 0xff, 0xbd, 0x05,
+	0xb2, 0x2c, 0x6a, 0x97, 0x4b, 0x13, 0x36, 0xfb, 0x48, 0x5d, 0x8d, 0xd2, 0xc4, 0xd6, 0x72, 0x9a,
+	0xd8, 0x1a, 0xde, 0x05, 0x80, 0x05, 0x7b, 0xaf, 0xf3, 0x10, 0x0d, 0x3d, 0x15, 0x56, 0x32, 0x3b,
+	0xab, 0x0d, 0x75, 0x3c, 0xd2, 0xae, 0x46, 0x50, 0x89, 0x47, 0xa2, 0x85, 0xef, 0x83, 0x3c, 0xf3,
+	0xd7, 0xf0, 0x10, 0xc2, 0xbc, 0x1a, 0x2e, 0x0e, 0xdc, 0xd5, 0x20, 0x70, 0x39, 0xc6, 0xd4, 0x46,
+	0x08, 0xf3, 0x90, 0x4d, 0x56, 0xf0, 0x10, 0xe4, 0x99, 0x70, 0x83, 0x0e, 0x07, 0x48, 0xcd, 0x04,
+	0xe2, 0x52, 0x6b, 0xe6, 0x78, 0x38, 0x40, 0x8d, 0x6b, 0xe3, 0x91, 0x06, 0x71, 0xb0, 0x92, 0x2c,
+	0xcc, 0x85, 0x30, 0x78, 0x0f, 0xac, 0x4e, 0x04, 0x1a, 0xb6, 0xc5, 0x6b, 0x27, 0x1b, 0xf9, 0xc6,
+	0x68, 0x9a, 0x56, 0xdc, 0x37, 0x01, 0x85, 0xbb, 0x60, 0x99, 0x9a, 0x36, 0xa6, 0x9e, 0xba, 0xc4,
+	0xab, 0x77, 0xbb, 0x26, 0x76, 0x62, 0xcd, 0x1c, 0xd8, 0x35, 0xb6, 0x5b, 0x6b, 0xe7, 0xef, 0xd6,
+	0x8e, 0x19, 0x45, 0x63, 0x2d, 0xf0, 0x2b, 0x60, 0xd0, 0x83, 0x5f, 0x78, 0x04, 0x96, 0x1d, 0xb3,
+	0x83, 0x1c, 0x4f, 0x5d, 0xe6, 0x22, 0xaa, 0xe9, 0xce, 0xd4, 0x1e, 0x71, 0xa2, 0x7d, 0x4c, 0xdd,
+	0x61, 0xe3, 0xea, 0x78, 0xa4, 0x15, 0x05, 0x97, 0x64, 0x58, 0x20, 0x07, 0x1a, 0x60, 0x9d, 0x12,
+	0x6a, 0x3a, 0x46, 0xb8, 0xf3, 0x3d, 0x75, 0xe5, 0xc5, 0xf6, 0x03, 0x67, 0x0f, 0x51, 0x9e, 0x1e,
+	0x5b, 0xc3, 0xbf, 0x28, 0xe0, 0x0d, 0xd3, 0x71, 0x48, 0xd7, 0xa4, 0x66, 0xc7, 0x41, 0x46, 0x67,
+	0x68, 0x0c, 0x5c, 0x9b, 0xb8, 0x36, 0x1d, 0x1a, 0x26, 0xb6, 0x26, 0x7a, 0xd5, 0x1c, 0xf7, 0xe8,
+	0x07, 0x33, 0x3c, 0xda, 0x8d, 0x44, 0x34, 0x86, 0x47, 0x81, 0x80, 0x5d, 0x6c, 0x85, 0x8a, 0x84,
+	0xaf, 0x3b, 0x81, 0x51, 0x15, 0x73, 0x0e, 0xb9, 0x3e, 0x97, 0x02, 0xba, 0x60, 0xd3, 0xa3, 0x26,
+	0xe5, 0x16, 0x07, 0xdb, 0x8c, 0x65, 0x3c, 0xcf, 0xcd, 0x7c, 0x7b, 0x86, 0x99, 0x6d, 0xc6, 0xd1,
+	0x18, 0x8a, 0xbd, 0xd5, 0xb4, 0x84, 0x55, 0xd7, 0x03, 0xab, 0xd6, 0xbd, 0x69, 0xac, 0x1e, 0x07,
+	0x40, 0x1f, 0x6c, 0x06, 0x76, 0x21, 0x2b, 0xd4, 0x6b, 0x5b, 0x2a, 0xe0, 0x3a, 0x6f, 0x5f, 0x1c,
+	0x1a, 0x64, 0x71, 0x41, 0xa1, 0x52, 0x35, 0x50, 0x5a, 0x34, 0x63, 0x68, 0x3d, 0x01, 0x81, 0x14,
+	0xc0, 0x29, 0xb5, 0x4f, 0x7d, 0xe4, 0x23, 0xb5, 0x70, 0x59, 0xad, 0x8f, 0x19, 0xf9, 0x6c, 0xad,
+	0x1c, 0xad, 0x27, 0x20, 0xcc, 0x59, 0x74, 0x6e, 0x77, 0x69, 0xd4, 0xc6, 0x0c, 0xdb, 0xf2, 0xd4,
+	0xb5, 0x0b, 0xd5, 0xee, 0x0b, 0x8e, 0x30, 0x62, 0x5e, 0x4c, 0x2d, 0x8a, 0xa1, 0xf5, 0x04, 0x04,
+	0x7e, 0xa9, 0x80, 0x32, 0x26, 0xd8, 0x30, 0xdd, 0xbe, 0x69, 0x99, 0x46, 0xe4, 0x78, 0xb4, 0x03,
+	0xae, 0x70, 0x13, 0xbe, 0x37, 0xc3, 0x84, 0x16, 0xc1, 0xbb, 0x9c, 0x77, 0x12, 0x82, 0x49, 0xb5,
+	0x0b, 0x6b, 0x5e, 0x0f, 0xac, 0xb9, 0x81, 0x67, 0x53, 0xea, 0x17, 0x21, 0xe1, 0x2e, 0xb8, 0xe2,
+	0xe3, 0x40, 0x3b, 0xab, 0x50, 0x75, 0xbd, 0xa2, 0xec, 0xe4, 0x1a, 0x37, 0xc6, 0x23, 0xed, 0xfa,
+	0x14, 0x42, 0xda, 0xd1, 0xd3, 0x1c, 0xf0, 0x13, 0x05, 0x5c, 0x0f, 0x3d, 0x32, 0x7c, 0xcf, 0xec,
+	0xa1, 0x28, 0xb3, 0x45, 0xee, 0xdf, 0x77, 0x66, 0xf8, 0x17, 0x9a, 0x71, 0xc2, 0x98, 0xa6, 0xb2,
+	0x5b, 0x1d, 0x8f, 0xb4, 0xb2, 0x9b, 0x82, 0x96, 0xcc, 0xb8, 0x9a, 0x86, 0x67, 0xa7, 0x96, 0x8b,
+	0x06, 0xc4, 0xa5, 0x36, 0xee, 0x19, 0x51, 0x4b, 0xde, 0xe0, 0x07, 0x09, 0x3f, 0xb5, 0x26, 0xe8,
+	0x56, 0xb2, 0xff, 0x6e, 0x24, 0x90, 0x25, 0x13, 0x14, 0xa4, 0x26, 0x07, 0x5f, 0x07, 0x99, 0x33,
+	0x34, 0x0c, 0x0e, 0xaf, 0x8d, 0xf1, 0x48, 0xbb, 0x72, 0x86, 0x86, 0x92, 0x04, 0x86, 0x85, 0x6f,
+	0x81, 0xa5, 0x73, 0xd3, 0xf1, 0x51, 0x30, 0x66, 0xf0, 0x29, 0x81, 0x03, 0xe4, 0x29, 0x81, 0x03,
+	0xee, 0x2d, 0xde, 0x55, 0x4a, 0x7f, 0x50, 0xc0, 0xb7, 0x2e, 0xd5, 0x76, 0x64, 0xed, 0x4b, 0x33,
+	0xb5, 0x37, 0x65, 0xed, 0xf3, 0xfb, 0xeb, 0x3c, 0xeb, 0x7e, 0xad, 0x80, 0xab, 0x69, 0xdd, 0xe6,
+	0x72, 0xa1, 0x78, 0x20, 0x1b, 0xb3, 0x76, 0xe7, 0x56, 0xd2, 0x18, 0x21, 0x54, 0x68, 0x98, 0x67,
+	0xcb, 0x27, 0x0a, 0xd8, 0x4a, 0xed, 0x42, 0x97, 0x33, 0xe6, 0x7f, 0x1c, 0x99, 0x98, 0x35, 0x51,
+	0xfd, 0xbe, 0x12, 0x6b, 0xce, 0xc0, 0x56, 0x6a, 0xcf, 0xfa, 0x06, 0x25, 0x9b, 0x9b, 0xab, 0xec,
+	0xf7, 0x0a, 0xa8, 0xcc, 0x6b, 0x4f, 0xaf, 0xa4, 0x5a, 0x7f, 0xa3, 0x80, 0xed, 0x99, 0x7d, 0xe5,
+	0x55, 0xe4, 0xa5, 0xfa, 0xc7, 0x2c, 0xc8, 0x85, 0xdd, 0x84, 0x8d, 0xbe, 0x4d, 0x31, 0xfa, 0x66,
+	0xc5, 0xe8, 0x3b, 0x35, 0xc4, 0x2d, 0x4e, 0x0d, 0x6f, 0x8b, 0xdf, 0x74, 0x78, 0x3b, 0x9e, 0x0c,
+	0x6f, 0xe2, 0xf6, 0xf2, 0xe6, 0xec, 0x49, 0xf4, 0x05, 0x06, 0xb8, 0x5f, 0x2a, 0x00, 0xfa, 0xd8,
+	0x43, 0xb4, 0x89, 0x2d, 0xf4, 0x11, 0xb2, 0x04, 0xa7, 0x9a, 0xe5, 0x2a, 0xee, 0x5c, 0xa0, 0xe2,
+	0x24, 0xc1, 0x24, 0xd4, 0x55, 0xc6, 0x23, 0xed, 0x66, 0x52, 0xa2, 0xa4, 0x3a, 0x45, 0xdf, 0xff,
+	0xa3, 0x1f, 0xf7, 0xc1, 0xf5, 0x19, 0x36, 0xbf, 0x0c, 0x75, 0xd5, 0x7f, 0x65, 0xc0, 0x36, 0xaf,
+	0xd1, 0xfb, 0x8e, 0xef, 0x51, 0xe4, 0x4e, 0x95, 0x2f, 0x6c, 0x82, 0x95, 0xae, 0x8b, 0xd8, 0xee,
+	0xe2, 0x5a, 0x2f, 0xbe, 0xa6, 0x6c, 0x06, 0x15, 0x11, 0xb2, 0xf0, 0x5b, 0x4a, 0xb8, 0x60, 0x76,
+	0x89, 0x63, 0x59, 0xb2, 0xeb, 0x69, 0xec, 0x54, 0x15, 0x14, 0xec, 0x62, 0x85, 0x82, 0x5b, 0x75,
+	0xd3, 0xe2, 0x17, 0x9a, 0xbc, 0xb8, 0x7c, 0x44, 0x50, 0xf9, 0xf2, 0x11, 0x41, 0xe1, 0xef, 0x14,
+	0x76, 0x02, 0x07, 0x7d, 0x20, 0x3a, 0xca, 0x82, 0x3a, 0xd9, 0x4b, 0xd6, 0xc9, 0x4c, 0xd7, 0x27,
+	0xdb, 0x4c, 0x12, 0x23, 0x2a, 0xe7, 0x46, 0xe0, 0x66, 0x9a, 0x22, 0x3d, 0x0d, 0x58, 0xfa, 0x54,
+	0x01, 0xea, 0x2c, 0x71, 0xaf, 0xa2, 0x4f, 0x55, 0xff, 0x9a, 0x01, 0xa5, 0x34, 0xa7, 0x75, 0x3e,
+	0x80, 0x4c, 0xde, 0x29, 0x94, 0x39, 0xef, 0x14, 0x52, 0x75, 0x2c, 0xfe, 0x97, 0xd5, 0xf1, 0xa9,
+	0x02, 0x8a, 0x52, 0xe8, 0x78, 0x5a, 0x82, 0x06, 0xd2, 0x48, 0x3a, 0x3b, 0xdb, 0x76, 0x39, 0x6d,
+	0xd2, 0x48, 0x57, 0x1e, 0x8f, 0xb4, 0x52, 0x5c, 0xbe, 0xe4, 0x4f, 0x42, 0x77, 0xe9, 0x0b, 0x05,
+	0x6c, 0xa5, 0xca, 0xba, 0xdc, 0x2e, 0xfc, 0xf1, 0x74, 0xc2, 0xde, 0x7e, 0x81, 0xca, 0x9b, 0x9b,
+	0xbd, 0x5f, 0x2d, 0x82, 0x55, 0x39, 0xdd, 0xf0, 0x03, 0x90, 0x8f, 0xa6, 0x7a, 0x85, 0x07, 0xed,
+	0x9d, 0x8b, 0x2b, 0xa4, 0x16, 0x9b, 0xe5, 0x37, 0x82, 0xe4, 0x44, 0x72, 0xf4, 0xe8, 0xb3, 0xf4,
+	0xb9, 0x02, 0xd6, 0x66, 0x9f, 0xae, 0xb3, 0x83, 0xf0, 0xd3, 0xe9, 0x20, 0xd4, 0xa4, 0xc3, 0x64,
+	0xf2, 0x26, 0x57, 0x1b, 0x9c, 0xf5, 0xf8, 0xe9, 0x12, 0xaa, 0xab, 0x3d, 0xf6, 0x4d, 0x4c, 0x6d,
+	0x3a, 0x9c, 0x1b, 0x87, 0xcf, 0x97, 0xc0, 0xc6, 0x01, 0xe9, 0xb4, 0x85, 0xa3, 0x36, 0xee, 0x35,
+	0xf1, 0x29, 0x81, 0x77, 0x40, 0xce, 0xb1, 0x4f, 0x11, 0xb5, 0xfb, 0x88, 0x9b, 0x77, 0x45, 0xbc,
+	0x77, 0x84, 0x30, 0xf9, 0xbd, 0x23, 0x84, 0xc1, 0x7b, 0x60, 0xd5, 0xa4, 0x46, 0x9f, 0x78, 0xd4,
+	0x20, 0xb8, 0x1b, 0x8e, 0x21, 0xbc, 0xe5, 0x98, 0xf4, 0x7d, 0xe2, 0xd1, 0x43, 0xdc, 0x95, 0x39,
+	0x41, 0x04, 0x85, 0xdf, 0x07, 0x85, 0x81, 0x8b, 0x18, 0xdc, 0x66, 0x57, 0x98, 0x0c, 0x67, 0xdd,
+	0x1e, 0x8f, 0xb4, 0x2d, 0x09, 0x2c, 0xf1, 0xca, 0xd4, 0xf0, 0x01, 0x28, 0x76, 0x09, 0xee, 0xfa,
+	0xae, 0x8b, 0x70, 0x77, 0x68, 0x78, 0xe6, 0xa9, 0x78, 0xa8, 0xcb, 0x35, 0x6e, 0x8d, 0x47, 0xda,
+	0xb6, 0x84, 0x6b, 0x9b, 0xa7, 0xb2, 0x94, 0xf5, 0x18, 0x8a, 0x5d, 0x3d, 0x26, 0x0f, 0x0e, 0x5d,
+	0xc7, 0xf4, 0x3c, 0x83, 0xbf, 0x61, 0x2d, 0x47, 0x57, 0x8f, 0x10, 0x7d, 0x9f, 0x61, 0x5b, 0xd3,
+	0x0f, 0x5a, 0x1b, 0x09, 0x24, 0x6c, 0x83, 0x82, 0xe7, 0x77, 0xfa, 0x36, 0x35, 0x78, 0x28, 0x57,
+	0xe6, 0x6e, 0xf0, 0xf0, 0xa9, 0x04, 0x08, 0xb6, 0xc9, 0xd3, 0x9e, 0xb4, 0x66, 0xc9, 0x09, 0x35,
+	0xa9, 0xb9, 0x28, 0x39, 0x21, 0x4c, 0x4e, 0x4e, 0x08, 0x83, 0xbf, 0x00, 0x9b, 0xa2, 0x84, 0x0d,
+	0x17, 0x3d, 0xf5, 0x6d, 0x17, 0xf5, 0x51, 0xf4, 0xba, 0xf4, 0x46, 0xb2, 0xce, 0x0f, 0xf9, 0xaf,
+	0x2e, 0xd1, 0x8a, 0xc3, 0x9e, 0x24, 0xe0, 0xf2, 0x61, 0x9f, 0xc4, 0xc2, 0x3a, 0x58, 0x39, 0x47,
+	0xae, 0x67, 0x13, 0xac, 0xe6, 0xb9, 0xad, 0x5b, 0xe3, 0x91, 0xb6, 0x11, 0x80, 0x24, 0xde, 0x90,
+	0xea, 0x5e, 0xf6, 0x8b, 0x2f, 0x35, 0xa5, 0xfa, 0x5b, 0x05, 0xc0, 0xa4, 0x0d, 0xd0, 0x01, 0xeb,
+	0x03, 0x62, 0xc9, 0xa0, 0xe0, 0x48, 0x7d, 0x2d, 0xe9, 0xc2, 0xd1, 0x34, 0xa1, 0x28, 0x86, 0x18,
+	0x77, 0x64, 0xc0, 0x83, 0x05, 0x3d, 0x2e, 0xba, 0xb1, 0x06, 0x56, 0xe5, 0x68, 0x55, 0xff, 0xb6,
+	0x02, 0xd6, 0x63, 0x52, 0xa1, 0x27, 0x5e, 0xf9, 0xda, 0xc8, 0x41, 0x5d, 0x4a, 0xdc, 0xa0, 0x73,
+	0xbc, 0x37, 0xd7, 0x1c, 0x3e, 0x5c, 0x85, 0x5c, 0xa2, 0x7f, 0x94, 0xc6, 0x23, 0xed, 0x9a, 0x2c,
+	0x4c, 0x0a, 0xcf, 0x94, 0x12, 0x78, 0x04, 0x72, 0xe6, 0xe9, 0xa9, 0x8d, 0x59, 0x05, 0x88, 0xb6,
+	0x70, 0x33, 0x6d, 0xc6, 0xdc, 0x0d, 0x68, 0x44, 0x7d, 0x84, 0x1c, 0x72, 0x7d, 0x84, 0x30, 0x78,
+	0x02, 0x0a, 0x94, 0x38, 0xc8, 0x35, 0xa9, 0x4d, 0x70, 0x38, 0x75, 0x96, 0x53, 0x07, 0xd7, 0x09,
+	0xd9, 0xe4, 0x34, 0x92, 0x59, 0x75, 0x79, 0x01, 0x09, 0x28, 0x98, 0x18, 0x13, 0x1a, 0x88, 0x5d,
+	0x99, 0x35, 0x69, 0xc6, 0x83, 0xb3, 0x1b, 0x31, 0x89, 0xd8, 0xf0, 0x5e, 0x20, 0x89, 0x92, 0x7b,
+	0x81, 0x04, 0x9e, 0xda, 0x1b, 0x59, 0x3e, 0x0d, 0xcc, 0xdf, 0x1b, 0x07, 0xa0, 0x18, 0xb6, 0x13,
+	0x82, 0x8f, 0x88, 0x63, 0x77, 0x87, 0xfc, 0x21, 0x3e, 0x2f, 0x4e, 0xbc, 0x38, 0x4e, 0x3e, 0xf1,
+	0xe2, 0x38, 0xf8, 0x31, 0x98, 0x3c, 0x6a, 0x4c, 0x55, 0xe9, 0x32, 0xcf, 0xd2, 0x4e, 0x5a, 0x40,
+	0xf5, 0x14, 0xfa, 0xc6, 0xcd, 0x20, 0xb4, 0xa9, 0xd2, 0xf4, 0x54, 0x28, 0x6c, 0x83, 0xcd, 0xae,
+	0xc9, 0x22, 0x1b, 0x35, 0xf3, 0x87, 0x48, 0xb4, 0x88, 0xd5, 0xc6, 0x6b, 0xe3, 0x91, 0x76, 0x2b,
+	0x05, 0x2d, 0x79, 0x93, 0xc6, 0x5d, 0xea, 0x81, 0x8d, 0x44, 0xa5, 0xbe, 0x94, 0x91, 0xfd, 0x14,
+	0x14, 0xe3, 0x59, 0x7f, 0x29, 0xb3, 0xfa, 0x9f, 0x14, 0xb0, 0x7d, 0xe4, 0x3b, 0x9e, 0xe9, 0xb6,
+	0xc3, 0x2a, 0x3c, 0x20, 0x9d, 0x3d, 0x44, 0x4d, 0xdb, 0xf1, 0x98, 0x30, 0xfe, 0x24, 0x11, 0xe8,
+	0xe4, 0xc2, 0x38, 0x40, 0x16, 0x26, 0x5e, 0x42, 0xdf, 0x02, 0x4b, 0x8f, 0xe3, 0xb3, 0x78, 0x7c,
+	0x24, 0x12, 0x14, 0xf0, 0x36, 0x58, 0x66, 0x67, 0x2c, 0xa2, 0xc1, 0x1c, 0xce, 0xaf, 0x69, 0x02,
+	0x22, 0x5f, 0xd3, 0x04, 0xe4, 0xdb, 0x87, 0xa0, 0x20, 0xbd, 0xa8, 0xc0, 0x02, 0x58, 0x39, 0x69,
+	0x3d, 0x6c, 0x1d, 0xfe, 0xa4, 0x55, 0x5c, 0x60, 0x8b, 0xa3, 0xfd, 0xd6, 0x5e, 0xb3, 0xf5, 0xa3,
+	0xa2, 0xc2, 0x16, 0xfa, 0x49, 0xab, 0xc5, 0x16, 0x8b, 0xf0, 0x0a, 0xc8, 0xb7, 0x4f, 0xee, 0xdf,
+	0xdf, 0xdf, 0xdf, 0xdb, 0xdf, 0x2b, 0x66, 0x20, 0x00, 0xcb, 0x3f, 0xdc, 0x6d, 0x3e, 0xda, 0xdf,
+	0x2b, 0x66, 0x1b, 0x3f, 0xff, 0xea, 0x59, 0x59, 0xf9, 0xfa, 0x59, 0x59, 0xf9, 0xe7, 0xb3, 0xb2,
+	0xf2, 0xd9, 0xf3, 0xf2, 0xc2, 0xd7, 0xcf, 0xcb, 0x0b, 0x7f, 0x7f, 0x5e, 0x5e, 0xf8, 0xd9, 0x7d,
+	0xe9, 0xaf, 0x3a, 0xf1, 0xc8, 0x39, 0x70, 0x09, 0xdb, 0x92, 0xc1, 0xaa, 0x7e, 0x89, 0xff, 0x24,
+	0x3b, 0xcb, 0xfc, 0x1c, 0x7b, 0xef, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xfd, 0xfc, 0x96, 0xed,
+	0xc1, 0x1c, 0x00, 0x00,
 }
 
 func (m *Executor) Marshal() (dAtA []byte, err error) {
@@ -1231,6 +1242,17 @@ func (m *Node) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	_ = i
 	var l int
 	_ = l
+	if len(m.NodeDbKeys) > 0 {
+		for iNdEx := len(m.NodeDbKeys) - 1; iNdEx >= 0; iNdEx-- {
+			i -= len(m.NodeDbKeys[iNdEx])
+			copy(dAtA[i:], m.NodeDbKeys[iNdEx])
+			i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.NodeDbKeys[iNdEx])))
+			i--
+			dAtA[i] = 0x1
+			i--
+			dAtA[i] = 0x92
+		}
+	}
 	if len(m.ReportingNodeType) > 0 {
 		i -= len(m.ReportingNodeType)
 		copy(dAtA[i:], m.ReportingNodeType)
@@ -1459,12 +1481,10 @@ func (m *Node) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			dAtA[i] = 0x2a
 		}
 	}
-	if len(m.NodeTypeId) > 0 {
-		i -= len(m.NodeTypeId)
-		copy(dAtA[i:], m.NodeTypeId)
-		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.NodeTypeId)))
+	if m.NodeTypeId != 0 {
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(m.NodeTypeId))
 		i--
-		dAtA[i] = 0x22
+		dAtA[i] = 0x20
 	}
 	if m.NodeType != nil {
 		{
@@ -1568,12 +1588,10 @@ func (m *NodeType) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 			dAtA[i] = 0x12
 		}
 	}
-	if len(m.Id) > 0 {
-		i -= len(m.Id)
-		copy(dAtA[i:], m.Id)
-		i = encodeVarintSchedulerobjects(dAtA, i, uint64(len(m.Id)))
+	if m.Id != 0 {
+		i = encodeVarintSchedulerobjects(dAtA, i, uint64(m.Id))
 		i--
-		dAtA[i] = 0xa
+		dAtA[i] = 0x8
 	}
 	return len(dAtA) - i, nil
 }
@@ -2126,9 +2144,8 @@ func (m *Node) Size() (n int) {
 		l = m.NodeType.Size()
 		n += 1 + l + sovSchedulerobjects(uint64(l))
 	}
-	l = len(m.NodeTypeId)
-	if l > 0 {
-		n += 1 + l + sovSchedulerobjects(uint64(l))
+	if m.NodeTypeId != 0 {
+		n += 1 + sovSchedulerobjects(uint64(m.NodeTypeId))
 	}
 	if len(m.Taints) > 0 {
 		for _, e := range m.Taints {
@@ -2222,6 +2239,12 @@ func (m *Node) Size() (n int) {
 	if l > 0 {
 		n += 2 + l + sovSchedulerobjects(uint64(l))
 	}
+	if len(m.NodeDbKeys) > 0 {
+		for _, b := range m.NodeDbKeys {
+			l = len(b)
+			n += 2 + l + sovSchedulerobjects(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -2231,9 +2254,8 @@ func (m *NodeType) Size() (n int) {
 	}
 	var l int
 	_ = l
-	l = len(m.Id)
-	if l > 0 {
-		n += 1 + l + sovSchedulerobjects(uint64(l))
+	if m.Id != 0 {
+		n += 1 + sovSchedulerobjects(uint64(m.Id))
 	}
 	if len(m.Taints) > 0 {
 		for _, e := range m.Taints {
@@ -2849,10 +2871,10 @@ func (m *Node) Unmarshal(dAtA []byte) error {
 			}
 			iNdEx = postIndex
 		case 4:
-			if wireType != 2 {
+			if wireType != 0 {
 				return fmt.Errorf("proto: wrong wireType = %d for field NodeTypeId", wireType)
 			}
-			var stringLen uint64
+			m.NodeTypeId = 0
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowSchedulerobjects
@@ -2862,24 +2884,11 @@ func (m *Node) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
+				m.NodeTypeId |= uint64(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			postIndex := iNdEx + intStringLen
-			if postIndex < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
-			}
-			m.NodeTypeId = string(dAtA[iNdEx:postIndex])
-			iNdEx = postIndex
 		case 5:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Taints", wireType)
@@ -4003,6 +4012,38 @@ func (m *Node) Unmarshal(dAtA []byte) error {
 			}
 			m.ReportingNodeType = string(dAtA[iNdEx:postIndex])
 			iNdEx = postIndex
+		case 18:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field NodeDbKeys", wireType)
+			}
+			var byteLen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowSchedulerobjects
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				byteLen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if byteLen < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			postIndex := iNdEx + byteLen
+			if postIndex < 0 {
+				return ErrInvalidLengthSchedulerobjects
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.NodeDbKeys = append(m.NodeDbKeys, make([]byte, postIndex-iNdEx))
+			copy(m.NodeDbKeys[len(m.NodeDbKeys)-1], dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipSchedulerobjects(dAtA[iNdEx:])
@@ -4054,10 +4095,10 @@ func (m *NodeType) Unmarshal(dAtA []byte) error {
 		}
 		switch fieldNum {
 		case 1:
-			if wireType != 2 {
+			if wireType != 0 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
 			}
-			var stringLen uint64
+			m.Id = 0
 			for shift := uint(0); ; shift += 7 {
 				if shift >= 64 {
 					return ErrIntOverflowSchedulerobjects
@@ -4067,24 +4108,11 @@ func (m *NodeType) Unmarshal(dAtA []byte) error {
 				}
 				b := dAtA[iNdEx]
 				iNdEx++
-				stringLen |= uint64(b&0x7F) << shift
+				m.Id |= uint64(b&0x7F) << shift
 				if b < 0x80 {
 					break
 				}
 			}
-			intStringLen := int(stringLen)
-			if intStringLen < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			postIndex := iNdEx + intStringLen
-			if postIndex < 0 {
-				return ErrInvalidLengthSchedulerobjects
-			}
-			if postIndex > l {
-				return io.ErrUnexpectedEOF
-			}
-			m.Id = string(dAtA[iNdEx:postIndex])
-			iNdEx = postIndex
 		case 2:
 			if wireType != 2 {
 				return fmt.Errorf("proto: wrong wireType = %d for field Taints", wireType)
diff --git a/internal/scheduler/schedulerobjects/schedulerobjects.proto b/internal/scheduler/schedulerobjects/schedulerobjects.proto
index a68b8e39..c9865a28 100644
--- a/internal/scheduler/schedulerobjects/schedulerobjects.proto
+++ b/internal/scheduler/schedulerobjects/schedulerobjects.proto
@@ -31,6 +31,8 @@ message Node {
     string id = 1;
     // Kubernetes node name. Not guaranteed to be unique across clusters.
     string name = 12;
+    // Keys by which nodes are sorted in the nodeDb.
+    repeated bytes nodeDbKeys = 18;
     // Time at which this node was last updated.
     // Used to garbage collect nodes that have been removed.
     google.protobuf.Timestamp last_seen = 2 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true];
@@ -40,7 +42,7 @@ message Node {
     NodeType node_type = 3;
     // We store NodeType.id here to simplify indexing.
     // This field is set automatically by the NodeDb on inserting the node.
-    string node_type_id = 4;
+    uint64 node_type_id = 4;
     // Kubernetes taints.
     repeated k8s.io.api.core.v1.Taint taints = 5 [(gogoproto.nullable) = false];
     // Kubernetes labels.
@@ -84,7 +86,7 @@ enum JobRunState {
 // the scheduler only considers nodes with a NodeType for which the taints and labels match.
 message NodeType {
     // Unique identifier. Used for map lookup.
-    string Id = 1;
+    uint64 Id = 1;
     // Kubernetes taints.
     // To reduce the number of distinct node types,
     // may contain only a subset of the taints of the node the node type is created from.
diff --git a/internal/scheduler/scheduling_algo.go b/internal/scheduler/scheduling_algo.go
index 38799edb..1025d6b2 100644
--- a/internal/scheduler/scheduling_algo.go
+++ b/internal/scheduler/scheduling_algo.go
@@ -42,7 +42,7 @@ type FairSchedulingAlgo struct {
 	queueRepository             database.QueueRepository
 	schedulingContextRepository *SchedulingContextRepository
 	priorityClasses             map[string]configuration.PriorityClass
-	indexedResources            []string
+	indexedResources            []configuration.IndexedResource
 	rand                        *rand.Rand // injected here for repeatable testing
 	previousScheduleClusterId   string
 	maxSchedulingDuration       time.Duration
@@ -61,19 +61,13 @@ func NewFairSchedulingAlgo(
 	if _, ok := config.Preemption.PriorityClasses[config.Preemption.DefaultPriorityClass]; !ok {
 		return nil, errors.Errorf("default priority class %s is missing from priority class mapping %v", config.Preemption.DefaultPriorityClass, config.Preemption.PriorityClasses)
 	}
-
-	indexedResources := config.IndexedResources
-	if len(indexedResources) == 0 {
-		indexedResources = []string{"cpu", "memory"}
-	}
-
 	algo := &FairSchedulingAlgo{
 		config:                      config,
 		executorRepository:          executorRepository,
 		queueRepository:             queueRepository,
 		schedulingContextRepository: schedulingContextRepository,
 		priorityClasses:             config.Preemption.PriorityClasses,
-		indexedResources:            indexedResources,
+		indexedResources:            config.IndexedResources,
 		maxSchedulingDuration:       maxSchedulingDuration,
 		rand:                        util.NewThreadsafeRand(time.Now().UnixNano()),
 		clock:                       clock.RealClock{},
diff --git a/internal/scheduler/submitcheck.go b/internal/scheduler/submitcheck.go
index 64d3924e..700cb541 100644
--- a/internal/scheduler/submitcheck.go
+++ b/internal/scheduler/submitcheck.go
@@ -44,7 +44,7 @@ type SubmitChecker struct {
 	gangIdAnnotation          string
 	executorById              map[string]minimalExecutor
 	priorities                []int32
-	indexedResources          []string
+	indexedResources          []configuration.IndexedResource
 	indexedTaints             []string
 	indexedNodeLabels         []string
 	executorRepository        database.ExecutorRepository


Test patch:
diff --git a/internal/scheduler/constraints/constraints_test.go b/internal/scheduler/constraints/constraints_test.go
index a9a71306..e387bf60 100644
--- a/internal/scheduler/constraints/constraints_test.go
+++ b/internal/scheduler/constraints/constraints_test.go
@@ -3,7 +3,9 @@ package constraints
 import (
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/api/resource"
 
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 )
@@ -31,3 +33,32 @@ func TestConstraints(t *testing.T) {
 		})
 	}
 }
+
+func TestScaleQuantity(t *testing.T) {
+	tests := map[string]struct {
+		input    resource.Quantity
+		f        float64
+		expected resource.Quantity
+	}{
+		"one": {
+			input:    resource.MustParse("1"),
+			f:        1,
+			expected: resource.MustParse("1"),
+		},
+		"zero": {
+			input:    resource.MustParse("1"),
+			f:        0,
+			expected: resource.MustParse("0"),
+		},
+		"rounding": {
+			input:    resource.MustParse("1"),
+			f:        0.3006,
+			expected: resource.MustParse("301m"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			assert.True(t, tc.expected.Equal(ScaleQuantity(tc.input, tc.f)), "expected %s, but got %s", tc.expected.String(), tc.input.String())
+		})
+	}
+}
diff --git a/internal/scheduler/gang_scheduler_test.go b/internal/scheduler/gang_scheduler_test.go
index 0c4dd2ce..b93fa5ed 100644
--- a/internal/scheduler/gang_scheduler_test.go
+++ b/internal/scheduler/gang_scheduler_test.go
@@ -12,6 +12,7 @@ import (
 	schedulerconstraints "github.com/armadaproject/armada/internal/scheduler/constraints"
 	schedulercontext "github.com/armadaproject/armada/internal/scheduler/context"
 	"github.com/armadaproject/armada/internal/scheduler/jobdb"
+	"github.com/armadaproject/armada/internal/scheduler/nodedb"
 	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
 	"github.com/armadaproject/armada/internal/scheduler/testfixtures"
 )
@@ -136,10 +137,54 @@ func TestGangScheduler(t *testing.T) {
 			},
 			ExpectedScheduledIndices: []int{1, 3, 5, 7},
 		},
+		"resolution has no impact on jobs of size a multiple of the resolution": {
+			SchedulingConfig: testfixtures.WithIndexedResourcesConfig(
+				[]configuration.IndexedResource{
+					{Name: "cpu", Resolution: resource.MustParse("16")},
+					{Name: "memory", Resolution: resource.MustParse("128Mi")},
+				},
+				testfixtures.TestSchedulingConfig(),
+			),
+			Nodes: testfixtures.N32CpuNodes(3, testfixtures.TestPriorities),
+			Gangs: [][]*jobdb.Job{
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+			},
+			ExpectedScheduledIndices: testfixtures.IntRange(0, 5),
+		},
+		"jobs of size not a multiple of the resolution blocks scheduling new jobs": {
+			SchedulingConfig: testfixtures.WithIndexedResourcesConfig(
+				[]configuration.IndexedResource{
+					{Name: "cpu", Resolution: resource.MustParse("17")},
+					{Name: "memory", Resolution: resource.MustParse("128Mi")},
+				},
+				testfixtures.TestSchedulingConfig(),
+			),
+			Nodes: testfixtures.N32CpuNodes(3, testfixtures.TestPriorities),
+			Gangs: [][]*jobdb.Job{
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+				testfixtures.N16CpuJobs("A", testfixtures.PriorityClass0, 1),
+			},
+			ExpectedScheduledIndices: testfixtures.IntRange(0, 2),
+		},
 	}
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
-			nodeDb, err := CreateNodeDb(tc.Nodes)
+			nodeDb, err := nodedb.NewNodeDb(
+				testfixtures.TestPriorityClasses,
+				testfixtures.TestMaxExtraNodesToConsider,
+				tc.SchedulingConfig.IndexedResources,
+				testfixtures.TestIndexedTaints,
+				testfixtures.TestIndexedNodeLabels,
+			)
+			require.NoError(t, err)
+			err = nodeDb.UpsertMany(tc.Nodes)
 			require.NoError(t, err)
 			if tc.TotalResources.Resources == nil {
 				// Default to NodeDb total.
diff --git a/internal/scheduler/nodedb/encoding_test.go b/internal/scheduler/nodedb/encoding_test.go
new file mode 100644
index 00000000..fb7f986d
--- /dev/null
+++ b/internal/scheduler/nodedb/encoding_test.go
@@ -0,0 +1,233 @@
+package nodedb
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"k8s.io/apimachinery/pkg/api/resource"
+
+	"github.com/armadaproject/armada/internal/scheduler/schedulerobjects"
+)
+
+// The memdb internally uses bytes.Compare to compare keys.
+// Here, we test that byte representation comparison of quantities works as expected.
+func TestEncodeQuantity(t *testing.T) {
+	tests := map[string]struct {
+		A resource.Quantity
+		B resource.Quantity
+	}{
+		"10Mi 10Mi": {
+			A: resource.MustParse("10Mi"),
+			B: resource.MustParse("10Mi"),
+		},
+		"5Mi 10Mi": {
+			A: resource.MustParse("5Mi"),
+			B: resource.MustParse("10Mi"),
+		},
+		"10Gi 10Gi": {
+			A: resource.MustParse("10Gi"),
+			B: resource.MustParse("10Gi"),
+		},
+		"5Gi 10Gi": {
+			A: resource.MustParse("5Gi"),
+			B: resource.MustParse("10Gi"),
+		},
+		"1 1": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1"),
+		},
+		"1 2": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("2"),
+		},
+		"-1 1": {
+			A: resource.MustParse("-1"),
+			B: resource.MustParse("1"),
+		},
+		"100m 100m": {
+			A: resource.MustParse("100M"),
+			B: resource.MustParse("100M"),
+		},
+		"100m 200m": {
+			A: resource.MustParse("100M"),
+			B: resource.MustParse("200M"),
+		},
+		"54870m 54871m": {
+			A: resource.MustParse("54870m"),
+			B: resource.MustParse("54871m"),
+		},
+		"1000Ti 1001Ti": {
+			A: resource.MustParse("1000Ti"),
+			B: resource.MustParse("1001Ti"),
+		},
+		"1000Pi 1001Pi": {
+			A: resource.MustParse("1000Pi"),
+			B: resource.MustParse("1001Pi"),
+		},
+		"1 1001m": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1001m"),
+		},
+		"1 1000m": {
+			A: resource.MustParse("1"),
+			B: resource.MustParse("1000m"),
+		},
+		"1Gi 1001Mi": {
+			A: resource.MustParse("1Gi"),
+			B: resource.MustParse("1001Mi"),
+		},
+		"1Gi 1000Mi": {
+			A: resource.MustParse("1Gi"),
+			B: resource.MustParse("1000Mi"),
+		},
+		"5188205838208Ki 5188205838209Ki": {
+			A: resource.MustParse("5188205838208Ki"),
+			B: resource.MustParse("5188205838209Ki"),
+		},
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			expected := tc.A.Cmp(tc.B)
+			actual := bytes.Compare(EncodeQuantity(nil, tc.A), EncodeQuantity(nil, tc.B))
+			assert.Equal(t, expected, actual)
+
+			expected = tc.B.Cmp(tc.A)
+			actual = bytes.Compare(EncodeQuantity(nil, tc.B), EncodeQuantity(nil, tc.A))
+			assert.Equal(t, expected, actual)
+		})
+	}
+}
+
+func TestNodeIndexKey(t *testing.T) {
+	type v struct {
+		nodeTypeId uint64
+		resources  []resource.Quantity
+	}
+	tests := map[string]struct {
+		a v
+		b v
+	}{
+		"equal nodeTypeId": {
+			a: v{
+				nodeTypeId: 10,
+			},
+			b: v{
+				nodeTypeId: 10,
+			},
+		},
+		"unequal nodeTypeId": {
+			a: v{
+				nodeTypeId: 10,
+			},
+			b: v{
+				nodeTypeId: 11,
+			},
+		},
+		"equal nodeTypeId and resources": {
+			a: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+			b: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+		},
+		"equal nodeTypeId and unequal resources": {
+			a: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("2"), resource.MustParse("1")},
+			},
+			b: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+		},
+		"unequal nodeTypeId and equal resources": {
+			a: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+			b: v{
+				nodeTypeId: 11,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+		},
+		"negative resource": {
+			a: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")},
+			},
+			b: v{
+				nodeTypeId: 10,
+				resources:  []resource.Quantity{resource.MustParse("-1"), resource.MustParse("2")},
+			},
+		},
+	}
+	expectedCmp := func(a, b v) int {
+		if a.nodeTypeId < b.nodeTypeId {
+			return -1
+		} else if a.nodeTypeId > b.nodeTypeId {
+			return 1
+		}
+		for i, qa := range a.resources {
+			qb := b.resources[i]
+			if cmp := qa.Cmp(qb); cmp != 0 {
+				return cmp
+			}
+		}
+		return 0
+	}
+	for name, tc := range tests {
+		t.Run(name, func(t *testing.T) {
+			keyA := NodeIndexKey(nil, tc.a.nodeTypeId, tc.a.resources)
+			keyB := NodeIndexKey(nil, tc.b.nodeTypeId, tc.b.resources)
+			assert.Equal(
+				t,
+				expectedCmp(tc.a, tc.b),
+				bytes.Compare(keyA, keyB),
+				"comparison failed for %v, %v", tc.a, tc.b,
+			)
+		})
+	}
+}
+
+func TestRoundedNodeIndexKeyFromResourceList(t *testing.T) {
+	assert.Equal(
+		t,
+		NodeIndexKey(nil, 0, []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")}),
+		RoundedNodeIndexKeyFromResourceList(
+			nil,
+			0,
+			[]string{"foo", "bar"},
+			[]int64{1000, 2000},
+			schedulerobjects.ResourceList{
+				Resources: map[string]resource.Quantity{"foo": resource.MustParse("1"), "bar": resource.MustParse("2")},
+			},
+		),
+	)
+	assert.NotEqual(
+		t,
+		NodeIndexKey(nil, 0, []resource.Quantity{resource.MustParse("1"), resource.MustParse("2")}),
+		RoundedNodeIndexKeyFromResourceList(
+			nil,
+			0,
+			[]string{"foo", "bar"},
+			[]int64{1000, 1500},
+			schedulerobjects.ResourceList{
+				Resources: map[string]resource.Quantity{"foo": resource.MustParse("1"), "bar": resource.MustParse("2")},
+			},
+		),
+	)
+}
+
+func BenchmarkEncodeQuantityBuffer(b *testing.B) {
+	out := make([]byte, 8)
+	q := resource.MustParse("16Gi")
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		out = out[0:0]
+		EncodeQuantity(out, q)
+	}
+}
diff --git a/internal/scheduler/nodedb/nodedb_test.go b/internal/scheduler/nodedb/nodedb_test.go
index 93420ad9..c62e0abf 100644
--- a/internal/scheduler/nodedb/nodedb_test.go
+++ b/internal/scheduler/nodedb/nodedb_test.go
@@ -17,8 +17,8 @@ import (
 )
 
 func TestNodeDbSchema(t *testing.T) {
-	err := nodeDbSchema(testfixtures.TestPriorities, testfixtures.TestResources).Validate()
-	assert.NoError(t, err)
+	schema, _ := nodeDbSchema(testfixtures.TestPriorities, testfixtures.TestResourceNames)
+	assert.NoError(t, schema.Validate())
 }
 
 // Test the accounting of total resources across all nodes.
@@ -479,30 +479,24 @@ func BenchmarkUpsert100000(b *testing.B) {
 }
 
 func benchmarkSelectAndBindNodeToPod(nodes []*schedulerobjects.Node, reqs []*schedulerobjects.PodRequirements, b *testing.B) {
-	db, err := NewNodeDb(
+	nodeDb, err := NewNodeDb(
 		testfixtures.TestPriorityClasses,
 		testfixtures.TestMaxExtraNodesToConsider,
 		testfixtures.TestResources,
 		testfixtures.TestIndexedTaints,
 		testfixtures.TestIndexedNodeLabels,
 	)
-	if !assert.NoError(b, err) {
-		return
-	}
+	require.NoError(b, err)
 
-	err = db.UpsertMany(nodes)
-	if !assert.NoError(b, err) {
-		return
-	}
+	err = nodeDb.UpsertMany(nodes)
+	require.NoError(b, err)
 
 	b.ResetTimer()
 	for n := 0; n < b.N; n++ {
-		txn := db.db.Txn(true)
+		txn := nodeDb.Txn(true)
 		for _, req := range reqs {
-			_, err := db.SelectAndBindNodeToPodWithTxn(txn, req)
-			if !assert.NoError(b, err) {
-				return
-			}
+			_, err := nodeDb.SelectAndBindNodeToPodWithTxn(txn, req)
+			require.NoError(b, err)
 		}
 		txn.Abort()
 	}
@@ -622,7 +616,7 @@ func createNodeDb(nodes []*schedulerobjects.Node) (*NodeDb, error) {
 	return db, nil
 }
 
-func BenchmarkNodeDbtringFromPodRequirementsNotMetReason(b *testing.B) {
+func BenchmarkNodeDbStringFromPodRequirementsNotMetReason(b *testing.B) {
 	nodeDb := &NodeDb{
 		podRequirementsNotMetReasonStringCache: make(map[uint64]string, 128),
 	}
@@ -643,3 +637,17 @@ func randomString(n int) string {
 	}
 	return s
 }
+
+func GetTestNodeDb() *NodeDb {
+	nodeDb, err := NewNodeDb(
+		testfixtures.TestPriorityClasses,
+		0,
+		testfixtures.TestResources,
+		testfixtures.TestIndexedTaints,
+		testfixtures.TestIndexedNodeLabels,
+	)
+	if err != nil {
+		panic(err)
+	}
+	return nodeDb
+}
diff --git a/internal/scheduler/nodedb/nodeiteration_test.go b/internal/scheduler/nodedb/nodeiteration_test.go
index 78ea8fcb..3136622c 100644
--- a/internal/scheduler/nodedb/nodeiteration_test.go
+++ b/internal/scheduler/nodedb/nodeiteration_test.go
@@ -8,6 +8,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"golang.org/x/exp/maps"
 	"golang.org/x/exp/slices"
 	"k8s.io/apimachinery/pkg/api/resource"
 
@@ -68,20 +69,26 @@ func TestNodePairIterator(t *testing.T) {
 		nodes[i].Id = c
 	}
 
-	db, err := memdb.NewMemDB(nodeDbSchema(testfixtures.TestPriorities, testfixtures.TestResources))
+	nodeDb, err := createNodeDb(nil)
 	require.NoError(t, err)
+	for _, node := range nodes {
+		node.NodeDbKeys = make([][]byte, len(nodeDb.prioritiesToTryAssigningAt))
+		for i, p := range nodeDb.prioritiesToTryAssigningAt {
+			node.NodeDbKeys[i] = nodeDb.nodeDbKeyFromNode(node.NodeDbKeys[i], node, p)
+		}
+	}
 
-	txn := db.Txn(true)
+	txn := nodeDb.Txn(true)
 	require.NoError(t, txn.Insert("nodes", nodes[0]))
 	require.NoError(t, txn.Insert("nodes", nodes[1]))
 	txn.Commit()
-	txnA := db.Txn(false)
+	txnA := nodeDb.Txn(false)
 
-	txn = db.Txn(true)
+	txn = nodeDb.Txn(true)
 	require.NoError(t, txn.Delete("nodes", nodes[0]))
 	require.NoError(t, txn.Insert("nodes", nodes[2]))
 	txn.Commit()
-	txnB := db.Txn(false)
+	txnB := nodeDb.Txn(false)
 
 	it, err := NewNodePairIterator(txnA, txnB)
 	require.NoError(t, err)
@@ -110,7 +117,7 @@ func TestNodePairIterator(t *testing.T) {
 func TestNodeTypeIterator(t *testing.T) {
 	tests := map[string]struct {
 		nodes            []*schedulerobjects.Node
-		nodeTypeId       string
+		nodeTypeId       uint64
 		priority         int32
 		resourceRequests schedulerobjects.ResourceList
 		expected         []int
@@ -118,19 +125,19 @@ func TestNodeTypeIterator(t *testing.T) {
 		"only yield nodes of the right nodeType": {
 			nodes: armadaslices.Concatenate(
 				testfixtures.WithNodeTypeIdNodes(
-					"foo",
+					1,
 					testfixtures.N32CpuNodes(1, testfixtures.TestPriorities),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"bar",
+					2,
 					testfixtures.N32CpuNodes(2, testfixtures.TestPriorities),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"foo",
+					1,
 					testfixtures.N32CpuNodes(3, testfixtures.TestPriorities),
 				),
 			),
-			nodeTypeId:       "foo",
+			nodeTypeId:       1,
 			priority:         0,
 			resourceRequests: schedulerobjects.ResourceList{},
 			expected: armadaslices.Concatenate(
@@ -140,7 +147,7 @@ func TestNodeTypeIterator(t *testing.T) {
 		},
 		"filter nodes with insufficient resources and return in increasing order": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -159,14 +166,14 @@ func TestNodeTypeIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeId:       "foo",
+			nodeTypeId:       1,
 			priority:         0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("16")}},
 			expected:         []int{1, 0},
 		},
 		"filter nodes with insufficient resources at priority and return in increasing order": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -215,14 +222,14 @@ func TestNodeTypeIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeId:       "foo",
+			nodeTypeId:       1,
 			priority:         1,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("16")}},
 			expected:         []int{4, 7, 3, 6, 0, 1, 2},
 		},
 		"nested ordering": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -297,7 +304,7 @@ func TestNodeTypeIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeId: "foo",
+			nodeTypeId: 1,
 			priority:   0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{
 				"cpu":    resource.MustParse("16"),
@@ -307,7 +314,7 @@ func TestNodeTypeIterator(t *testing.T) {
 		},
 		"double-nested ordering": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -394,7 +401,7 @@ func TestNodeTypeIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeId: "foo",
+			nodeTypeId: 1,
 			priority:   0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{
 				"cpu":    resource.MustParse("32"),
@@ -406,23 +413,41 @@ func TestNodeTypeIterator(t *testing.T) {
 	}
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
+			nodeDb, err := createNodeDb(nil)
+			require.NoError(t, err)
+
 			// Set monotonically increaseing node ids to ensure nodes appear in predictable order.
 			for i, node := range tc.nodes {
 				node.Id = fmt.Sprintf("%d", i)
 			}
-
 			indexByNodeId := make(map[string]int)
 			for i, node := range tc.nodes {
 				indexByNodeId[node.Id] = i
 			}
-			db, err := newTestNodeDb(tc.nodes)
-			require.NoError(t, err)
 
+			// Compute the keys necessary to efficiently iterate over nodes
+			// and populate the database. We do this manually instead of using nodeDb.Upsert to control the nodeTypeId.
+			for _, node := range tc.nodes {
+				node.NodeDbKeys = make([][]byte, len(nodeDb.prioritiesToTryAssigningAt))
+				for i, p := range nodeDb.prioritiesToTryAssigningAt {
+					node.NodeDbKeys[i] = nodeDb.nodeDbKeyFromNode(node.NodeDbKeys[i], node, p)
+				}
+			}
+			require.NoError(t, populateDatabase(nodeDb.db, tc.nodes))
+
+			// Create iterator.
 			indexedResourceRequests := make([]resource.Quantity, len(testfixtures.TestResources))
-			for i, t := range testfixtures.TestResources {
+			for i, t := range nodeDb.indexedResources {
 				indexedResourceRequests[i] = tc.resourceRequests.Get(t)
 			}
-			it, err := NewNodeTypeIterator(db.Txn(false), tc.nodeTypeId, tc.priority, testfixtures.TestResources, indexedResourceRequests)
+			keyIndex := -1
+			for i, p := range nodeDb.prioritiesToTryAssigningAt {
+				if p == tc.priority {
+					keyIndex = i
+				}
+			}
+			require.NotEqual(t, -1, keyIndex)
+			it, err := NewNodeTypeIterator(nodeDb.Txn(false), tc.nodeTypeId, nodeIndexName(keyIndex), tc.priority, testfixtures.TestResourceNames, indexedResourceRequests)
 			require.NoError(t, err)
 
 			// Compare actual with expected order.
@@ -439,10 +464,12 @@ func TestNodeTypeIterator(t *testing.T) {
 			}
 			assert.Equal(t, tc.expected, actual)
 
-			// Calling next again should still return nil.
-			node, err := it.NextNode()
-			require.NoError(t, err)
-			require.Nil(t, node)
+			// Calling next should always return nil from now on.
+			for i := 0; i < 100; i++ {
+				node, err := it.NextNode()
+				require.NoError(t, err)
+				require.Nil(t, node)
+			}
 		})
 	}
 }
@@ -450,7 +477,7 @@ func TestNodeTypeIterator(t *testing.T) {
 func TestNodeTypesIterator(t *testing.T) {
 	tests := map[string]struct {
 		nodes            []*schedulerobjects.Node
-		nodeTypeIds      []string
+		nodeTypeIds      []uint64
 		priority         int32
 		resourceRequests schedulerobjects.ResourceList
 		expected         []int
@@ -458,19 +485,19 @@ func TestNodeTypesIterator(t *testing.T) {
 		"only yield nodes of the right nodeType": {
 			nodes: armadaslices.Concatenate(
 				testfixtures.WithNodeTypeIdNodes(
-					"foo",
+					1,
 					testfixtures.N32CpuNodes(1, testfixtures.TestPriorities),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"bar",
+					2,
 					testfixtures.N32CpuNodes(2, testfixtures.TestPriorities),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"baz",
+					3,
 					testfixtures.N32CpuNodes(3, testfixtures.TestPriorities),
 				),
 			),
-			nodeTypeIds:      []string{"foo", "baz"},
+			nodeTypeIds:      []uint64{1, 3},
 			priority:         0,
 			resourceRequests: schedulerobjects.ResourceList{},
 			expected: armadaslices.Concatenate(
@@ -481,7 +508,7 @@ func TestNodeTypesIterator(t *testing.T) {
 		"filter nodes with insufficient resources and return in increasing order": {
 			nodes: armadaslices.Concatenate(
 				testfixtures.WithNodeTypeIdNodes(
-					"foo",
+					1,
 					testfixtures.WithUsedResourcesNodes(
 						0,
 						schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("15")}},
@@ -489,7 +516,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"bar",
+					2,
 					testfixtures.WithUsedResourcesNodes(
 						0,
 						schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("16")}},
@@ -497,7 +524,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"baz",
+					3,
 					testfixtures.WithUsedResourcesNodes(
 						0,
 						schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("17")}},
@@ -505,7 +532,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"foobar",
+					4,
 					testfixtures.WithUsedResourcesNodes(
 						0,
 						schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("14")}},
@@ -513,14 +540,14 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeIds:      []string{"foo", "bar", "baz"},
+			nodeTypeIds:      []uint64{1, 2, 3},
 			priority:         0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("16")}},
 			expected:         []int{1, 0},
 		},
 		"filter nodes with insufficient resources at priority and return in increasing order": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -569,14 +596,14 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeIds:      []string{"foo"},
+			nodeTypeIds:      []uint64{1},
 			priority:         1,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": resource.MustParse("16")}},
 			expected:         []int{4, 7, 3, 6, 0, 1, 2},
 		},
 		"nested ordering": {
 			nodes: testfixtures.WithNodeTypeIdNodes(
-				"foo",
+				1,
 				armadaslices.Concatenate(
 					testfixtures.WithUsedResourcesNodes(
 						0,
@@ -651,7 +678,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeIds: []string{"foo"},
+			nodeTypeIds: []uint64{1},
 			priority:    0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{
 				"cpu":    resource.MustParse("16"),
@@ -662,7 +689,7 @@ func TestNodeTypesIterator(t *testing.T) {
 		"double-nested ordering": {
 			nodes: armadaslices.Concatenate(
 				testfixtures.WithNodeTypeIdNodes(
-					"foo",
+					1,
 					armadaslices.Concatenate(
 						testfixtures.WithUsedResourcesNodes(
 							0,
@@ -702,7 +729,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"bar",
+					2,
 					armadaslices.Concatenate(
 						testfixtures.WithUsedResourcesNodes(
 							0,
@@ -740,7 +767,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 				testfixtures.WithNodeTypeIdNodes(
-					"baz",
+					3,
 					armadaslices.Concatenate(
 						testfixtures.WithUsedResourcesNodes(
 							0,
@@ -760,7 +787,7 @@ func TestNodeTypesIterator(t *testing.T) {
 					),
 				),
 			),
-			nodeTypeIds: []string{"foo", "bar", "baz"},
+			nodeTypeIds: []uint64{1, 2, 3},
 			priority:    0,
 			resourceRequests: schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{
 				"cpu":    resource.MustParse("32"),
@@ -772,23 +799,40 @@ func TestNodeTypesIterator(t *testing.T) {
 	}
 	for name, tc := range tests {
 		t.Run(name, func(t *testing.T) {
+			nodeDb, err := createNodeDb(nil)
+			require.NoError(t, err)
+
 			// Set monotonically increaseing node ids to ensure nodes appear in predictable order.
 			for i, node := range tc.nodes {
 				node.Id = fmt.Sprintf("%d", i)
 			}
-
 			indexByNodeId := make(map[string]int)
 			for i, node := range tc.nodes {
 				indexByNodeId[node.Id] = i
 			}
-			db, err := newTestNodeDb(tc.nodes)
-			require.NoError(t, err)
+
+			// Compute the keys necessary to efficiently iterate over nodes
+			// and populate the database. We do this manually instead of using nodeDb.Upsert to control the nodeTypeId.
+			for _, node := range tc.nodes {
+				node.NodeDbKeys = make([][]byte, len(nodeDb.prioritiesToTryAssigningAt))
+				for i, p := range nodeDb.prioritiesToTryAssigningAt {
+					node.NodeDbKeys[i] = nodeDb.nodeDbKeyFromNode(node.NodeDbKeys[i], node, p)
+				}
+			}
+			require.NoError(t, populateDatabase(nodeDb.db, tc.nodes))
 
 			indexedResourceRequests := make([]resource.Quantity, len(testfixtures.TestResources))
-			for i, t := range testfixtures.TestResources {
+			for i, t := range testfixtures.TestResourceNames {
 				indexedResourceRequests[i] = tc.resourceRequests.Get(t)
 			}
-			it, err := NewNodeTypesIterator(db.Txn(false), tc.nodeTypeIds, tc.priority, testfixtures.TestResources, indexedResourceRequests)
+			it, err := NewNodeTypesIterator(
+				nodeDb.Txn(false),
+				tc.nodeTypeIds,
+				nodeDb.indexNameByPriority[tc.priority],
+				tc.priority,
+				testfixtures.TestResourceNames,
+				indexedResourceRequests,
+			)
 			require.NoError(t, err)
 
 			// Compare actual with expected order.
@@ -813,18 +857,6 @@ func TestNodeTypesIterator(t *testing.T) {
 	}
 }
 
-func newTestNodeDb(nodes []*schedulerobjects.Node) (*memdb.MemDB, error) {
-	db, err := memdb.NewMemDB(nodeDbSchema(testfixtures.TestPriorities, testfixtures.TestResources))
-	if err != nil {
-		return nil, err
-	}
-	err = populateDatabase(db, nodes)
-	if err != nil {
-		return nil, err
-	}
-	return db, nil
-}
-
 func populateDatabase(db *memdb.MemDB, items []*schedulerobjects.Node) error {
 	txn := db.Txn(true)
 	defer txn.Abort()
@@ -837,3 +869,47 @@ func populateDatabase(db *memdb.MemDB, items []*schedulerobjects.Node) error {
 	txn.Commit()
 	return nil
 }
+
+func BenchmarkNodeTypeIterator(b *testing.B) {
+	// Create nodes with varying amounts of CPU available.
+	numNodes := 1000
+	allocatedMilliCpus := []int64{
+		1, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900,
+		2, 2100, 2200, 2300, 2400, 2500, 2600, 2700, 2800, 2900,
+		3, 4, 5, 6, 7, 8, 9,
+	}
+	nodes := testfixtures.N32CpuNodes(numNodes, testfixtures.TestPriorities)
+	for i, node := range nodes {
+		var q resource.Quantity
+		q.SetMilli(allocatedMilliCpus[i%len(allocatedMilliCpus)])
+		testfixtures.WithUsedResourcesNodes(
+			testfixtures.TestPriorities[len(testfixtures.TestPriorities)-1],
+			schedulerobjects.ResourceList{Resources: map[string]resource.Quantity{"cpu": q}},
+			[]*schedulerobjects.Node{node},
+		)
+	}
+	nodeDb, err := createNodeDb(nodes)
+	require.NoError(b, err)
+
+	// Create iterator for 0 CPU required and an unfeasible memory request,
+	// such that the iterator has to consider all nodes.
+	indexedResourceRequests := make([]resource.Quantity, len(nodeDb.indexedResources))
+	indexedResourceRequests[1] = resource.MustParse("1Ti")
+	nodeTypeId := maps.Keys(nodeDb.nodeTypes)[0]
+	var priority int32
+	txn := nodeDb.Txn(false)
+	defer txn.Abort()
+
+	b.ResetTimer()
+	for n := 0; n < b.N; n++ {
+		it, err := NewNodeTypeIterator(txn, nodeTypeId, nodeDb.indexNameByPriority[priority], priority, nodeDb.indexedResources, indexedResourceRequests)
+		require.NoError(b, err)
+		for {
+			node, err := it.NextNode()
+			require.NoError(b, err)
+			if node == nil {
+				break
+			}
+		}
+	}
+}
diff --git a/internal/scheduler/schedulerobjects/quantity_test.go b/internal/scheduler/schedulerobjects/quantity_test.go
deleted file mode 100644
index 6575718d..00000000
--- a/internal/scheduler/schedulerobjects/quantity_test.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package schedulerobjects
-
-import (
-	"bytes"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"k8s.io/apimachinery/pkg/api/resource"
-)
-
-// The memdb internally uses bytes.Compare to compare keys.
-// Here, we test that byte representation comparison of quantities works as expected.
-func TestQuantityIndexComparison(t *testing.T) {
-	tests := map[string]struct {
-		A resource.Quantity
-		B resource.Quantity
-	}{
-		"10Mi 10Mi": {
-			A: resource.MustParse("10Mi"),
-			B: resource.MustParse("10Mi"),
-		},
-		"5Mi 10Mi": {
-			A: resource.MustParse("5Mi"),
-			B: resource.MustParse("10Mi"),
-		},
-		"10Gi 10Gi": {
-			A: resource.MustParse("10Gi"),
-			B: resource.MustParse("10Gi"),
-		},
-		"5Gi 10Gi": {
-			A: resource.MustParse("5Gi"),
-			B: resource.MustParse("10Gi"),
-		},
-		"1 1": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1"),
-		},
-		"1 2": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("2"),
-		},
-		"-1 1": {
-			A: resource.MustParse("-1"),
-			B: resource.MustParse("1"),
-		},
-		"100m 100m": {
-			A: resource.MustParse("100M"),
-			B: resource.MustParse("100M"),
-		},
-		"100m 200m": {
-			A: resource.MustParse("100M"),
-			B: resource.MustParse("200M"),
-		},
-		"54870m 54871m": {
-			A: resource.MustParse("54870m"),
-			B: resource.MustParse("54871m"),
-		},
-		"1000Ti 1001Ti": {
-			A: resource.MustParse("1000Ti"),
-			B: resource.MustParse("1001Ti"),
-		},
-		"1000Pi 1001Pi": {
-			A: resource.MustParse("1000Pi"),
-			B: resource.MustParse("1001Pi"),
-		},
-		"1 1001m": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1001m"),
-		},
-		"1 1000m": {
-			A: resource.MustParse("1"),
-			B: resource.MustParse("1000m"),
-		},
-		"1Gi 1001Mi": {
-			A: resource.MustParse("1Gi"),
-			B: resource.MustParse("1001Mi"),
-		},
-		"1Gi 1000Mi": {
-			A: resource.MustParse("1Gi"),
-			B: resource.MustParse("1000Mi"),
-		},
-		"5188205838208Ki 5188205838209Ki": {
-			A: resource.MustParse("5188205838208Ki"),
-			B: resource.MustParse("5188205838209Ki"),
-		},
-	}
-	for name, tc := range tests {
-		t.Run(name, func(t *testing.T) {
-			expected := tc.A.Cmp(tc.B)
-			actual := bytes.Compare(EncodeQuantity(tc.A), EncodeQuantity(tc.B))
-			assert.Equal(t, expected, actual)
-
-			expected = tc.B.Cmp(tc.A)
-			actual = bytes.Compare(EncodeQuantity(tc.B), EncodeQuantity(tc.A))
-			assert.Equal(t, expected, actual)
-		})
-	}
-}
-
-func TestScaleQuantity(t *testing.T) {
-	tests := map[string]struct {
-		input    resource.Quantity
-		f        float64
-		expected resource.Quantity
-	}{
-		"one": {
-			input:    resource.MustParse("1"),
-			f:        1,
-			expected: resource.MustParse("1"),
-		},
-		"zero": {
-			input:    resource.MustParse("1"),
-			f:        0,
-			expected: resource.MustParse("0"),
-		},
-		"rounding": {
-			input:    resource.MustParse("1"),
-			f:        0.3006,
-			expected: resource.MustParse("301m"),
-		},
-	}
-	for name, tc := range tests {
-		t.Run(name, func(t *testing.T) {
-			assert.True(t, tc.expected.Equal(ScaleQuantity(tc.input, tc.f)), "expected %s, but got %s", tc.expected.String(), tc.input.String())
-		})
-	}
-}
diff --git a/internal/scheduler/testfixtures/testfixtures.go b/internal/scheduler/testfixtures/testfixtures.go
index e5255f5f..826053a6 100644
--- a/internal/scheduler/testfixtures/testfixtures.go
+++ b/internal/scheduler/testfixtures/testfixtures.go
@@ -48,10 +48,22 @@ var (
 	TestDefaultPriorityClass         = PriorityClass3
 	TestPriorities                   = []int32{0, 1, 2, 3}
 	TestMaxExtraNodesToConsider uint = 1
-	TestResources                    = []string{"cpu", "memory", "gpu"}
-	TestIndexedTaints                = []string{"largeJobsOnly", "gpu"}
-	TestIndexedNodeLabels            = []string{"largeJobsOnly", "gpu"}
-	jobTimestamp                atomic.Int64
+	TestResources                    = []configuration.IndexedResource{
+		{Name: "cpu", Resolution: resource.MustParse("1")},
+		{Name: "memory", Resolution: resource.MustParse("128Mi")},
+		{Name: "gpu", Resolution: resource.MustParse("1")},
+	}
+	TestResourceNames = util.Map(
+		TestResources,
+		func(v configuration.IndexedResource) string { return v.Name },
+	)
+	TestIndexedResourceResolutionMillis = util.Map(
+		TestResources,
+		func(v configuration.IndexedResource) int64 { return v.Resolution.MilliValue() },
+	)
+	TestIndexedTaints     = []string{"largeJobsOnly", "gpu"}
+	TestIndexedNodeLabels = []string{"largeJobsOnly", "gpu"}
+	jobTimestamp          atomic.Int64
 )
 
 func IntRange(a, b int) []int {
@@ -83,7 +95,7 @@ func TestSchedulingConfig() configuration.SchedulingConfig {
 			NodeEvictionProbability:                 1.0,
 			NodeOversubscriptionEvictionProbability: 1.0,
 		},
-		IndexedResources:                 []string{"cpu", "memory"},
+		IndexedResources:                 TestResources,
 		ExecutorTimeout:                  15 * time.Minute,
 		MaxUnacknowledgedJobsPerExecutor: math.MaxInt,
 	}
@@ -125,6 +137,11 @@ func WithPerPriorityLimitsConfig(limits map[int32]map[string]float64, config con
 	return config
 }
 
+func WithIndexedResourcesConfig(indexResources []configuration.IndexedResource, config configuration.SchedulingConfig) configuration.SchedulingConfig {
+	config.IndexedResources = indexResources
+	return config
+}
+
 func WithMaxJobsToScheduleConfig(n uint, config configuration.SchedulingConfig) configuration.SchedulingConfig {
 	config.MaximumJobsToSchedule = n
 	return config
@@ -163,7 +180,7 @@ func WithUsedResourcesNodes(p int32, rl schedulerobjects.ResourceList, nodes []*
 	return nodes
 }
 
-func WithNodeTypeIdNodes(nodeTypeId string, nodes []*schedulerobjects.Node) []*schedulerobjects.Node {
+func WithNodeTypeIdNodes(nodeTypeId uint64, nodes []*schedulerobjects.Node) []*schedulerobjects.Node {
 	for _, node := range nodes {
 		node.NodeTypeId = nodeTypeId
 	}
@@ -495,8 +512,8 @@ func TestCluster() []*schedulerobjects.Node {
 	return []*schedulerobjects.Node{
 		{
 			Id:         "node1",
-			NodeTypeId: "foo",
-			NodeType:   &schedulerobjects.NodeType{Id: "foo"},
+			NodeTypeId: 1,
+			NodeType:   &schedulerobjects.NodeType{Id: 1},
 			AllocatableByPriorityAndResource: map[int32]schedulerobjects.ResourceList{
 				0: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("1"), "memory": resource.MustParse("1Gi")}},
 				1: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("2"), "memory": resource.MustParse("2Gi")}},
@@ -514,8 +531,8 @@ func TestCluster() []*schedulerobjects.Node {
 		},
 		{
 			Id:         "node2",
-			NodeTypeId: "foo",
-			NodeType:   &schedulerobjects.NodeType{Id: "foo"},
+			NodeTypeId: 2,
+			NodeType:   &schedulerobjects.NodeType{Id: 2},
 			AllocatableByPriorityAndResource: map[int32]schedulerobjects.ResourceList{
 				0: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("4"), "memory": resource.MustParse("4Gi")}},
 				1: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("5"), "memory": resource.MustParse("5Gi")}},
@@ -533,8 +550,8 @@ func TestCluster() []*schedulerobjects.Node {
 		},
 		{
 			Id:         "node3",
-			NodeTypeId: "bar",
-			NodeType:   &schedulerobjects.NodeType{Id: "bar"},
+			NodeTypeId: 3,
+			NodeType:   &schedulerobjects.NodeType{Id: 3},
 			AllocatableByPriorityAndResource: map[int32]schedulerobjects.ResourceList{
 				0: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("7"), "memory": resource.MustParse("7Gi")}},
 				1: {Resources: map[string]resource.Quantity{"cpu": resource.MustParse("8"), "memory": resource.MustParse("8Gi")}},