mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool
@ 2026-08-27  3:58 Jim Cromie
  2026-08-27  3:58 ` [PATCH 1/8] lockdep: Traverse adjacency lists directly in zap_class() Jim Cromie
                   ` (8 more replies)
  0 siblings, 9 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Lockdep cannot rely upon any other subsystem that uses locks, so since
inception, its graph-db has been stored in static arrays, pinning ~10
MB in .bss. This is a hardcoded compromise between embedded and
enterprise hardware.

However, if it acts early, lockdep can pre-allocate a pool of slabs
from memblock_alloc(), enough for its lifetime of anticipated workloads.
Then it can allocate them as needed to provide new segments/slabs to
the graph-db.

With that idea, we:

0. Add lockdep_early_init() hook in start_kernel() right before
   mm_core_init() to grab a private pool of 64 KB slabs from memblock.

1. Add DECLARE_CHUNKED_ARRAY() to build 2D chunk pointer tables.
   Indexing uses a compile-time hybrid:
   - Power-of-2 tables (lock_chains @ 2,048/slab, chain_hlocks @ 32,768/slab)
     use single-cycle bit shifts (idx >> SHIFT) and masks (idx & MASK)
     for zero-overhead cache verification.
   - Non-power-of-2 structs (lock_classes @ 409/slab, list_entries @ 1,365/slab)
     use Granlund-Montgomery reciprocal divide to achieve >99.8% slab
     packing density, avoiding 1.75 MB of internal dead padding.

2. Deploy chunked arrays across the 5 graph-db tables:
   - lock_classes: struct lock_class (160 B) -> lock_class_chunk0 (409 / slab)
   - list_entries: struct lock_list  (48 B)  -> list_entries_chunk0 (1,365 / slab)
   - lock_chains:  struct lock_chain (32 B)  -> lock_chain_chunk0 (2,048 / slab)
   - chain_hlocks: u16               (2 B)   -> chain_hlock_chunk0 (32,768 / slab)
   - stack_trace:  unsigned long     (8 B)   -> stack_trace_chunk0 (8,192 / slab)
   Each static name##_chunk0 in .bss (~320 kB total) provisions the graph-db
   with initial storage to cover early boot until memblock is up.

3. Embed struct lock_class.class_idx and struct lock_chain.chain_idx to
   replace flat pointer arithmetic (ptr - base) with O(1) index queries
   across disjoint 2D slabs.

4. Dole slabs out on demand to the 5 consumers via an index bump under
   graph_lock (zero allocator locks, zero recursion risk).

5. Auto-tune the pool size based on RAM and accept boot overrides via
   lockdep_slabs=N and lockdep_headroom=M%.

6. At late_initcall, satisfy both constraints (slabs >= N and headroom
   >= M%), and return all unused excess slabs to the buddy allocator
   via free_reserved_page().

7. Expose pool usage and remaining headroom via /proc/lockdep_stats and
   log lifetime usage via a reboot notifier.

8. On debug_locks_off() or OOM, immediately sacrifice all dynamically
   claimed slabs back to the buddy allocator.

Static .bss Memory Savings (vmlinux x86_64 defconfig):

    Kernel                 .bss Section Size       Notes
    ----------------------------------------------------------------------
    Upstream (Static)      12.46 MB (13061164 B)   Fixed max-sized arrays
    Patched (Memblock)      2.30 MB ( 2410988 B)   5 * 64 kB Chunk 0s in .bss
    ----------------------------------------------------------------------
    Net Savings            -10.16 MB (81.5% reduction in .bss)

Memblock-Pool Elasticity & Buddy Return:

  [ 0.850318] lockdep: boot complete : 9/64 slabs used, 41 kept (355% headroom), 23 returned to buddy (1472 kB freed)

  That VM boot consumed 9 slabs (576 kB), keeps 41 slabs (2624 kB,
  355% headroom) for runtime growth, and returns 23 slabs (1472 kB) to
  buddy at late_initcall:

  The boot-args let user specify the reserved-slab-pool size:
    lockdep_slabs=N		# min ct of 64kb slabs kept
    lockdep_headroom=N%		# added % to boot-complete numbers, default is 100%

Workload Stress Performance & CPU Overheads (perf stat, 4 vCPUs):

    Benchmark     Metric          Upstream (Base)     Patched (Memblock)  Delta
    ---------------------------------------------------------------------------
    hackbench     Runtime             8.482 s             8.278 s        -2.40%
    hackbench     Cycles          52899510936         52033166458        -1.64%
    hackbench     Instructions    29008189069         31698626928        +9.27%
    netns         Runtime             9.704 s            13.321 s       +37.28%
    netns         Cycles           3941156282          5077381493       +28.83%
    netns         Instructions     2758079004          3008859260        +9.09%
    vfs           Runtime            10.544 s            10.608 s        +0.60%
    vfs           Cycles          48150692681         48840696114        +1.43%
    vfs           Instructions    32621052088         35448537908        +8.67%
    modstorm      Runtime             0.611 s             0.585 s        -4.17%
    modstorm      Cycles            512425082           514784953        +0.46%
    modstorm      Instructions      349890783           383054584        +9.48%

    Under heavy lock contention (hackbench), the power-of-2 fast-path on
    chain_hlocks and lock_chains brings total cycle consumption to parity
    with or slightly faster than upstream baseline (-1.64% cycles).

Workload specifics (virtme-ng, 4 vCPUs, 4 GB RAM):
- hackbench: hackbench -p -g 8 -l 1000
- netns:     40 netns add/del cycles with paired veth interfaces
- vfs:       8 parallel workers creating 200 dirs, files, symlinks + rm -rf
- modstorm:  10 sequential rounds of batch modprobe/rmmod (dummy loop null_blk brd tun)

What's Unchanged:
- All lockdep validation invariants, BFS graph algorithms, and deadlock
  detection logic are completely unmodified.
- RCU iteration semantics across lock classes and chains remain intact.
- /proc/lockdep and /proc/lockdep_stats formatting is fully preserved.

Series Structure:
- Patch 1: Optimize zap_class() to traverse adjacency lists directly
  rather than scanning the global bitmap.
- Patch 2: Add chunked array infrastructure and embedded indices.
- Patch 3: Pre-reserve early memblock slab pool for dynamic tables.
- Patch 4: Convert 5 graph arrays to chunked tables backed by slab pool.
- Patch 5: Fast-path power-of-2 tables with shift/mask indexing.
- Patch 6: Free unused reservation slabs to buddy allocator at late boot.
- Patch 7: Expose slab pool telemetry in /proc/lockdep_stats and initcalls.
- Patch 8: On debug_locks_off or OOM, recycle all slabs to buddy.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
Jim Cromie (8):
      lockdep: Traverse adjacency lists directly in zap_class()
      lockdep: Add chunked array infrastructure and embedded indices
      lockdep: Pre-reserve early memblock slab pool for dynamic tables
      lockdep: Convert 5 graph arrays to chunked tables backed by slab pool
      lockdep: Fast-path power-of-2 tables with shift/mask indexing
      lockdep: Free unused reservation slabs to buddy allocator at late boot
      lockdep: Expose slab pool telemetry in /proc/lockdep_stats and initcalls
      lockdep: on debug_locks_off or OOM, recycle all slabs to buddy

 include/linux/lockdep.h            |   4 +-
 include/linux/lockdep_types.h      |   1 +
 init/main.c                        |   1 +
 kernel/locking/lockdep.c           | 951 ++++++++++++++++++++++++++++---------
 kernel/locking/lockdep_internals.h |  95 +++-
 kernel/locking/lockdep_proc.c      |  63 ++-
 6 files changed, 875 insertions(+), 240 deletions(-)
---
base-commit: 8d3ae59288f1e7d58d76558a6ee96d533bc5019f
change-id: 20260825-lockdep-memblock-v1-12c083225ef9

Best regards,
-- 
Jim Cromie <jim.cromie@gmail.com>


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 1/8] lockdep: Traverse adjacency lists directly in zap_class()
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 2/8] lockdep: Add chunked array infrastructure and embedded indices Jim Cromie
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Lockdep's canonical graph representation is its per-class adjacency
lists (locks_after and locks_before). However, zap_class() operates on
a flat storage-layer projection of the graph: it scans the global
list_entries_in_use bitmap across the entire edge pool.

This global scan has a few defects:

0. Search Inefficiency:

On a typical booted laptop with ~2,000 lock classes and ~6,500 active
dependency list entries, zapping a single class forces 6,500+ table
inspections under graph_lock across 4 KB of bitmap. Zapping a batch of
classes during module unload multiplies this into tens of thousands of
global array iterations.

1. Projection maintenance: the bitmap must be kept up-to-date.
   given 0, its a net burden, but we still need the bitmap elsewhere.

2. Incompatible with Array Segmentation:

The loop relies on contiguous pointer arithmetic (list_entries + i) to
map bitmap indices back to entries. This completely breaks once
list_entries is segmented into dynamic 64 kB memblock slabs residing
on disjoint memory pages.

So just implement the adjacency check literally, per graph-theory.

Real-world lock classes have very short adjacency lists: 3..5 entries
on average for class->locks_after and class->locks_before, rarely
exceeding 15.

Directly walking these lists visits only ~10..15 nodes per zapped
class, replacing 6,500+ global table dereferences with a handful of
cacheline-local pointer hops (>99.8% reduction in loop iterations).

Note: We continue clearing bits in list_entries_in_use for now, as
alloc_list_entry() still queries the bitmap in this commit. The bitmap
itself is eliminated soon

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 2d4c5bab5af8..6a4f21f3e9c8 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -6243,8 +6243,7 @@ static void remove_class_from_lock_chains(struct pending_free *pf,
  */
 static void zap_class(struct pending_free *pf, struct lock_class *class)
 {
-	struct lock_list *entry;
-	int i;
+	struct lock_list *entry, *tmp, *other, *other_tmp;
 
 	WARN_ON_ONCE(!class->key);
 
@@ -6252,11 +6251,29 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	 * Remove all dependencies this lock is
 	 * involved in:
 	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		entry = list_entries + i;
-		if (entry->class != class && entry->links_to != class)
-			continue;
-		__clear_bit(i, list_entries_in_use);
+	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
+		nr_list_entries--;
+		list_del_rcu(&entry->entry);
+	}
+	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
+		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
+			if (other->links_to == class) {
+				__clear_bit(other - list_entries, list_entries_in_use);
+				nr_list_entries--;
+				list_del_rcu(&other->entry);
+				break;
+			}
+		}
+		__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
 	}

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 2/8] lockdep: Add chunked array infrastructure and embedded indices
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
  2026-08-27  3:58 ` [PATCH 1/8] lockdep: Traverse adjacency lists directly in zap_class() Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 3/8] lockdep: Pre-reserve early memblock slab pool for dynamic tables Jim Cromie
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Lockdep's dependency graph has historically relied on flat static
arrays in .bss. To transition these tables to dynamically allocated
slabs without incurring division instructions, introduce the
DECLARE_CHUNKED_ARRAY() and DEFINE_CHUNKED_ARRAY() macros.

These macros construct 2-tier chunked arrays (Array-of-Arrays) indexed
via Granlund-Montgomery reciprocal divide (reciprocal_divide()),
mapping indices to (chunk, offset) tuples in constant time (~3 cycles).

Also embed class_idx into struct lock_class and chain_idx into struct
lock_chain to replace flat pointer arithmetic (ptr - base) with O(1)
index lookups across disjoint slab chunks.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/lockdep.h            |  3 ++-
 include/linux/lockdep_types.h      |  1 +
 kernel/locking/lockdep_internals.h | 48 ++++++++++++++++++++++++++++++++++++--
 3 files changed, 49 insertions(+), 3 deletions(-)

diff --git a/include/linux/lockdep.h b/include/linux/lockdep.h
index 621566345406..4c96959d8ad7 100644
--- a/include/linux/lockdep.h
+++ b/include/linux/lockdep.h
@@ -77,7 +77,7 @@ struct lock_chain {
 	unsigned int			irq_context :  2,
 					depth       :  6,
 					base	    : 24;
-	/* 4 byte hole */
+	unsigned int			chain_idx;
 	struct hlist_node		entry;
 	u64				chain_key;
 };
@@ -85,6 +85,7 @@ struct lock_chain {
 /*
  * Initialization, self-test and debugging-output methods:
  */
+extern void lockdep_early_init(void);
 extern void lockdep_init(void);
 extern void lockdep_reset(void);
 extern void lockdep_reset_lock(struct lockdep_map *lock);
diff --git a/include/linux/lockdep_types.h b/include/linux/lockdep_types.h
index eae115a26488..8acac0b59f69 100644
--- a/include/linux/lockdep_types.h
+++ b/include/linux/lockdep_types.h
@@ -121,6 +121,7 @@ struct lock_class {
 
 	unsigned int			subclass;
 	unsigned int			dep_gen_id;
+	unsigned int			class_idx;
 
 	/*
 	 * IRQ/softirq usage tracking bits:
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index 0e5e6ffe91a3..3d8bce0dc9f9 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -122,9 +122,53 @@ enum {
 #define MAX_LOCKDEP_CHAINS	(1UL << MAX_LOCKDEP_CHAINS_BITS)
 
 #define AVG_LOCKDEP_CHAIN_DEPTH		5
-#define MAX_LOCKDEP_CHAIN_HLOCKS (MAX_LOCKDEP_CHAINS * AVG_LOCKDEP_CHAIN_DEPTH)
+#include <linux/reciprocal_div.h>
 
-extern struct lock_chain lock_chains[];
+#define LOCKDEP_SLAB_SIZE	(64 * 1024)
+#define LOCKDEP_MAX_SLABS	64
+
+/*
+ * Chunked Array Tables:
+ * Replaces flat monolithic BSS arrays with 2D chunk pointer matrices.
+ * Chunk 0 is statically allocated in BSS for early boot, while subsequent
+ * chunks are claimed from the memblock reservoir via lockdep_claim_slab().
+ * Indexing uses compile-time Granlund-Montgomery reciprocal divide
+ * (~3-cycle multiply+shift, zero division instructions).
+ */
+#define DECLARE_CHUNKED_ARRAY(name, type)					\
+	enum {									\
+		name##_PER_CHUNK = (LOCKDEP_SLAB_SIZE / sizeof(type)),		\
+	};									\
+	extern type * name##_chunks[LOCKDEP_MAX_SLABS];				\
+	extern const struct reciprocal_value name##_rv;				\
+	static __always_inline type *idx_to_##name(unsigned int idx)		\
+	{									\
+		unsigned int chunk = reciprocal_divide(idx, name##_rv);		\
+		unsigned int offset = idx - (chunk * name##_PER_CHUNK);		\
+		type *chunk_ptr;						\
+		if (unlikely(chunk >= LOCKDEP_MAX_SLABS))			\
+			return NULL;						\
+		/* Pairs with smp_store_release() when new chunk slabs are published */ \
+		chunk_ptr = smp_load_acquire(&name##_chunks[chunk]);		\
+		if (unlikely(!chunk_ptr))					\
+			return NULL;						\
+		return &chunk_ptr[offset];					\
+	}
+
+#define DEFINE_CHUNKED_ARRAY(name, type)					\
+	static type name##_chunk0[name##_PER_CHUNK];				\
+	type *name##_chunks[LOCKDEP_MAX_SLABS] = { name##_chunk0 };		\
+	static unsigned int nr_##name##_chunks = 1;				\
+	const struct reciprocal_value name##_rv =				\
+		RECIPROCAL_VALUE_INIT(name##_PER_CHUNK)
+
+struct lockdep_slab_usage {
+	unsigned int lock_classes;
+	unsigned int direct_deps;
+	unsigned int lock_chains;
+	unsigned int chain_hlocks;
+	unsigned int stack_traces;
+};
 
 #define LOCK_USAGE_CHARS (2*XXX_LOCK_USAGE_STATES + 1)
 

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 3/8] lockdep: Pre-reserve early memblock slab pool for dynamic tables
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
  2026-08-27  3:58 ` [PATCH 1/8] lockdep: Traverse adjacency lists directly in zap_class() Jim Cromie
  2026-08-27  3:58 ` [PATCH 2/8] lockdep: Add chunked array infrastructure and embedded indices Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 4/8] lockdep: Convert 5 graph arrays to chunked tables backed by slab pool Jim Cromie
                   ` (5 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Lockdep cannot allocate memory dynamically during normal runtime because
it cannot recurse into allocator locks. However, before mm_core_init()
brings up the buddy allocator, lockdep can claim a contiguous pool of
64 KB slabs directly from early memblock.

Add lockdep_early_init() to start_kernel() right before mm_core_init()
to reserve a private pool of 64 KB slabs. Auto-tune the pool based on
physical RAM (2 MB on <512 MB systems, 4 MB default, 8 MB on >64 GB
servers) and accept overrides via lockdep_slabs=N and
lockdep_headroom=M%.

Provide lockdep_claim_slab() and lockdep_release_slab() to dole out and
recycle slabs under graph_lock without invoking external locks.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 init/main.c                        |   1 +
 kernel/locking/lockdep.c           | 131 +++++++++++++++++++++++++++++++++++++
 kernel/locking/lockdep_internals.h |   3 +-
 3 files changed, 134 insertions(+), 1 deletion(-)

diff --git a/init/main.c b/init/main.c
index e363232b428b..12280dfe1d11 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1031,6 +1031,7 @@ void start_kernel(void)
 	vfs_caches_init_early();
 	sort_main_extable();
 	trap_init();
+	lockdep_early_init();
 	mm_core_init();
 	maple_tree_init();
 	poking_init();
diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 6a4f21f3e9c8..68d82e46cbf6 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -58,12 +58,96 @@
 #include <linux/context_tracking.h>
 #include <linux/console.h>
 #include <linux/kasan.h>
+#include <linux/memblock.h>
 
 #include <asm/sections.h>
 
 #include "lockdep_internals.h"
 #include "lock_events.h"
 
+static void *lockdep_slabs[LOCKDEP_MAX_SLABS];
+static unsigned int lockdep_nr_slabs;
+static unsigned int lockdep_slabs_used;
+static struct lockdep_slab_usage ld_slabs;
+
+static unsigned int requested_lockdep_slabs;
+static unsigned int requested_lockdep_headroom_pct = 100; /* default 100% headroom */
+static bool lockdep_headroom_specified;
+static bool lockdep_disabled_early;
+
+static int __init setup_lockdep_slabs(char *str)
+{
+	unsigned long val;
+
+	if (!str)
+		return -EINVAL;
+
+	if (!strcmp(str, "off") || !strcmp(str, "0")) {
+		lockdep_disabled_early = true;
+		return 0;
+	}
+
+	if (kstrtoul(str, 0, &val))
+		return -EINVAL;
+
+	if (val > 10000) {
+		pr_warn("lockdep: ignoring unrealistic lockdep_slabs=%lu\n",
+			val);
+		return -EINVAL;
+	}
+
+	requested_lockdep_slabs = clamp_t(unsigned int, val, 2, LOCKDEP_MAX_SLABS);
+	return 0;
+}
+early_param("lockdep_slabs", setup_lockdep_slabs);
+
+static int __init setup_lockdep_headroom(char *str)
+{
+	unsigned long val;
+
+	if (!str || kstrtoul(str, 0, &val))
+		return -EINVAL;
+
+	requested_lockdep_headroom_pct = clamp_t(unsigned int, val, 10, 900);
+	lockdep_headroom_specified = true;
+	return 0;
+}
+early_param("lockdep_headroom", setup_lockdep_headroom);
+
+static void *lockdep_free_slabs[LOCKDEP_MAX_SLABS];
+static unsigned int lockdep_nr_free_slabs;
+
+/*
+ * Claim a 64KB slab from the pre-allocated memblock reservoir.
+ * Must be called with graph_lock held. Completely lockless and deadlock-free.
+ */
+static void *lockdep_claim_slab(unsigned int *table_counter)
+{
+	void *slab;
+
+	if (lockdep_nr_free_slabs > 0)
+		slab = lockdep_free_slabs[--lockdep_nr_free_slabs];
+	else if (lockdep_slabs_used < lockdep_nr_slabs)
+		slab = lockdep_slabs[lockdep_slabs_used++];
+	else
+		return NULL;
+
+	if (table_counter)
+		(*table_counter)++;
+
+	return slab;
+}
+
+static void lockdep_release_slab(void *slab, unsigned int *table_counter)
+{
+	if (!slab || lockdep_nr_free_slabs >= LOCKDEP_MAX_SLABS)
+		return;
+
+	lockdep_free_slabs[lockdep_nr_free_slabs++] = slab;
+	if (table_counter && *table_counter > 0)
+		(*table_counter)--;
+}
+
 #include <trace/events/lock.h>
 
 #ifdef CONFIG_PROVE_LOCKING
@@ -6646,6 +6730,53 @@ void lockdep_unregister_key(struct lock_class_key *key)
 }
 EXPORT_SYMBOL_GPL(lockdep_unregister_key);
 
+void __init lockdep_early_init(void)
+{
+	unsigned int nr_slabs, i;
+	phys_addr_t phys_mem;
+	size_t slab_bytes;
+	void *pool;
+
+	if (lockdep_disabled_early) {
+		pr_info("lockdep: disabled by early boot parameter, 0 bytes reserved\n");
+		return;
+	}
+
+	phys_mem = memblock_phys_mem_size();
+
+	/* Auto-tune based on physical memory and CPU count */
+	if (phys_mem && phys_mem < (512ULL << 20))
+		nr_slabs = 32;   /* 2 MB on small systems (<512MB RAM) */
+	else if (num_possible_cpus() >= 64 || phys_mem > (64ULL << 30))
+		nr_slabs = 128;  /* 8 MB on large servers (>64GB RAM or >64 CPUs) */
+	else
+		nr_slabs = LOCKDEP_DEFAULT_SLABS; /* 64 slabs = 4 MB default */
+
+	/* Ensure initial reservation satisfies requested floor or headroom */
+	if (requested_lockdep_slabs > nr_slabs)
+		nr_slabs = requested_lockdep_slabs;
+
+	if (lockdep_headroom_specified && requested_lockdep_headroom_pct > 100)
+		nr_slabs = (nr_slabs * (100 + requested_lockdep_headroom_pct)) / 100;
+
+	nr_slabs = clamp_t(unsigned int, nr_slabs, 8, LOCKDEP_MAX_SLABS);
+
+	slab_bytes = (size_t)nr_slabs * LOCKDEP_SLAB_SIZE;
+	pool = memblock_alloc(slab_bytes, PAGE_SIZE);
+	if (!pool) {
+		pr_err("lockdep: failed to allocate %u slabs (%zu KB) from memblock\n",
+		       nr_slabs, slab_bytes / 1024);
+		return;
+	}
+
+	for (i = 0; i < nr_slabs; i++)
+		lockdep_slabs[i] = (char *)pool + (i * LOCKDEP_SLAB_SIZE);
+
+	lockdep_nr_slabs = nr_slabs;
+	pr_info("lockdep: reserved %u slabs (%zu KB) from memblock\n",
+		nr_slabs, slab_bytes / 1024);
+}
+
 void __init lockdep_init(void)
 {
 	pr_info("Lock dependency validator: Copyright (c) 2006 Red Hat, Inc., Ingo Molnar\n");
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index 3d8bce0dc9f9..3344361a1c3b 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -125,7 +125,8 @@ enum {
 #include <linux/reciprocal_div.h>
 
 #define LOCKDEP_SLAB_SIZE	(64 * 1024)
-#define LOCKDEP_MAX_SLABS	64
+#define LOCKDEP_MAX_SLABS	512
+#define LOCKDEP_DEFAULT_SLABS	64
 
 /*
  * Chunked Array Tables:

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 4/8] lockdep: Convert 5 graph arrays to chunked tables backed by slab pool
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (2 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 3/8] lockdep: Pre-reserve early memblock slab pool for dynamic tables Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 5/8] lockdep: Fast-path power-of-2 tables with shift/mask indexing Jim Cromie
                   ` (4 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Replace the monolithic static .bss tables for lock_classes, list_entries,
lock_chains, chain_hlocks, and stack_trace with 2D chunked arrays
(DECLARE_CHUNKED_ARRAY) backed by the early memblock slab pool.

Retain 1 static chunk (Chunk 0) in .bss per consumer (~320 kB total) to
cover early bootstrap locking before memblock is initialized:
- lock_classes: 409 items in Chunk 0 (160 B each)
- list_entries: 1,365 items in Chunk 0 (48 B each)
- lock_chains:  2,048 items in Chunk 0 (32 B each)
- chain_hlocks: 32,768 items in Chunk 0 (2 B each)
- stack_trace:  8,192 items in Chunk 0 (8 B each)

Update allocators (alloc_lock_chain(), alloc_chain_hlocks(),
alloc_list_entry(), save_trace()) to claim new 64 KB slabs dynamically
from the memblock reservoir under graph_lock when chunk capacity is
exhausted.

Replace flat array pointer arithmetic with embedded struct indices
(class->class_idx and chain->chain_idx).

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 include/linux/lockdep.h            |   1 +
 include/linux/lockdep_types.h      |   2 +-
 kernel/locking/lockdep.c           | 600 ++++++++++++++++++++++++-------------
 kernel/locking/lockdep_internals.h |  63 ++--
 4 files changed, 436 insertions(+), 230 deletions(-)

diff --git a/include/linux/lockdep.h b/include/linux/lockdep.h
index 4c96959d8ad7..0ebb9a3e1bea 100644
--- a/include/linux/lockdep.h
+++ b/include/linux/lockdep.h
@@ -340,6 +340,7 @@ static inline void lockdep_set_selftest_task(struct task_struct *task)
 # define lock_set_class(l, n, key, s, i)	do { (void)(key); } while (0)
 # define lock_set_novalidate_class(l, n, i)	do { } while (0)
 # define lock_set_subclass(l, s, i)		do { } while (0)
+# define lockdep_early_init()			do { } while (0)
 # define lockdep_init()				do { } while (0)
 # define lockdep_init_map_type(lock, name, key, sub, inner, outer, type) \
 		do { (void)(name); (void)(key); } while (0)
diff --git a/include/linux/lockdep_types.h b/include/linux/lockdep_types.h
index 8acac0b59f69..fa0dcf3c3f70 100644
--- a/include/linux/lockdep_types.h
+++ b/include/linux/lockdep_types.h
@@ -114,6 +114,7 @@ struct lock_class {
 	 * "backward" graph nodes.
 	 */
 	struct list_head		locks_after, locks_before;
+	unsigned int			class_idx;
 
 	const struct lockdep_subclass_key *key;
 	lock_cmp_fn			cmp_fn;
@@ -121,7 +122,6 @@ struct lock_class {
 
 	unsigned int			subclass;
 	unsigned int			dep_gen_id;
-	unsigned int			class_idx;
 
 	/*
 	 * IRQ/softirq usage tracking bits:
diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 68d82e46cbf6..1c8db52af1ac 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -58,7 +58,9 @@
 #include <linux/context_tracking.h>
 #include <linux/console.h>
 #include <linux/kasan.h>
+#include <linux/mm.h>
 #include <linux/memblock.h>
+#include <linux/reboot.h>
 
 #include <asm/sections.h>
 
@@ -148,6 +150,28 @@ static void lockdep_release_slab(void *slab, unsigned int *table_counter)
 		(*table_counter)--;
 }
 
+void lockdep_get_slab_stats(struct lockdep_slab_stats *st)
+{
+	st->total_slabs = lockdep_nr_slabs;
+	st->used_slabs = lockdep_slabs_used - lockdep_nr_free_slabs;
+	st->usage = ld_slabs;
+}
+
+#define BOOTSTRAP_LOCKDEP_ENTRIES 1024UL
+
+static struct lock_list list_entries[BOOTSTRAP_LOCKDEP_ENTRIES];
+static struct lock_list *list_entries_freelist;
+static struct lock_list *list_entries_cur = list_entries;
+static unsigned int list_entries_remaining = BOOTSTRAP_LOCKDEP_ENTRIES;
+
+static inline void free_list_entry(struct lock_list *entry)
+{
+	if (!entry)
+		return;
+	*(void **)entry = list_entries_freelist;
+	list_entries_freelist = entry;
+}
+
 #include <trace/events/lock.h>
 
 #ifdef CONFIG_PROVE_LOCKING
@@ -289,8 +313,6 @@ static inline int debug_locks_off_graph_unlock(void)
 }
 
 unsigned long nr_list_entries;
-static struct lock_list list_entries[MAX_LOCKDEP_ENTRIES];
-static DECLARE_BITMAP(list_entries_in_use, MAX_LOCKDEP_ENTRIES);
 
 /*
  * All data structures here are protected by the global debug_lock.
@@ -305,29 +327,25 @@ unsigned long nr_lock_classes;
 unsigned long nr_zapped_classes;
 unsigned long nr_dynamic_keys;
 unsigned long max_lock_class_idx;
-struct lock_class lock_classes[MAX_LOCKDEP_KEYS];
+
+DEFINE_CHUNKED_ARRAY(lock_class, struct lock_class);
+
+static void lockdep_print_watermarks(const char *bug_msg);
+
 DECLARE_BITMAP(lock_classes_in_use, MAX_LOCKDEP_KEYS);
 
 static inline struct lock_class *hlock_class(struct held_lock *hlock)
 {
 	unsigned int class_idx = hlock->class_idx;
 
-	/* Don't re-read hlock->class_idx, can't use READ_ONCE() on bitfield */
 	barrier();
 
 	if (!test_bit(class_idx, lock_classes_in_use)) {
-		/*
-		 * Someone passed in garbage, we give up.
-		 */
 		DEBUG_LOCKS_WARN_ON(1);
 		return NULL;
 	}
 
-	/*
-	 * At this point, if the passed hlock->class_idx is still garbage,
-	 * we just have to live with it
-	 */
-	return lock_classes + class_idx;
+	return idx_to_lock_class(class_idx);
 }
 
 #ifdef CONFIG_LOCK_STAT
@@ -388,7 +406,7 @@ void lock_stats(struct lock_class *class, struct lock_class_stats *stats)
 	memset(stats, 0, sizeof(struct lock_class_stats));
 	for_each_possible_cpu(cpu) {
 		struct lock_class_stats *pcs =
-			&per_cpu(cpu_lock_stats, cpu)[class - lock_classes];
+			&per_cpu(cpu_lock_stats, cpu)[class->class_idx];
 
 		for (i = 0; i < ARRAY_SIZE(stats->contention_point); i++)
 			stats->contention_point[i] += pcs->contention_point[i];
@@ -413,7 +431,7 @@ void clear_lock_stats(struct lock_class *class)
 
 	for_each_possible_cpu(cpu) {
 		struct lock_class_stats *cpu_stats =
-			&per_cpu(cpu_lock_stats, cpu)[class - lock_classes];
+			&per_cpu(cpu_lock_stats, cpu)[class->class_idx];
 
 		memset(cpu_stats, 0, sizeof(struct lock_class_stats));
 	}
@@ -423,7 +441,7 @@ void clear_lock_stats(struct lock_class *class)
 
 static struct lock_class_stats *get_lock_stats(struct lock_class *class)
 {
-	return &this_cpu_ptr(cpu_lock_stats)[class - lock_classes];
+	return &this_cpu_ptr(cpu_lock_stats)[class->class_idx];
 }
 
 static void lock_release_holdtime(struct held_lock *hlock)
@@ -555,9 +573,26 @@ static __always_inline void lockdep_recursion_finish(void)
 		__this_cpu_write(lockdep_recursion, 0);
 }
 
+static void lockdep_selftest_trace_start(void);
+static void lockdep_selftest_trace_finish(void);
+static void lockdep_report_stage(const char *domain, const char *stage_name);
+
 void lockdep_set_selftest_task(struct task_struct *task)
 {
-	lockdep_selftest_task_struct = task;
+	unsigned long flags;
+
+	if (task) {
+		lockdep_selftest_task_struct = task;
+		lockdep_selftest_trace_start();
+		return;
+	}
+
+	lockdep_selftest_task_struct = NULL;
+	raw_local_irq_save(flags);
+	lockdep_lock();
+	lockdep_selftest_trace_finish();
+	lockdep_unlock();
+	raw_local_irq_restore(flags);
 }
 
 /*
@@ -622,6 +657,7 @@ unsigned long nr_stack_trace_entries;
  * @nr_entries:	Number of entries in @entries.
  * @entries:	Actual stack backtrace.
  */
+#define STACK_TRACE_HASH_SIZE	(1 << CONFIG_LOCKDEP_STACK_TRACE_HASH_BITS)
 struct lock_trace {
 	struct hlist_node	hash_entry;
 	u32			hash;
@@ -630,17 +666,80 @@ struct lock_trace {
 };
 #define LOCK_TRACE_SIZE_IN_LONGS				\
 	(sizeof(struct lock_trace) / sizeof(unsigned long))
-/*
- * Stack-trace: sequence of lock_trace structures. Protected by the graph_lock.
- */
-static unsigned long stack_trace[MAX_STACK_TRACE_ENTRIES];
+#define BOOTSTRAP_STACK_TRACE_ENTRIES 4096UL
+#define MAX_LOCKDEP_TRACE_DEPTH 48
+
+static unsigned long stack_trace[BOOTSTRAP_STACK_TRACE_ENTRIES];
+static unsigned long *trace_free_ptr = stack_trace;
+static size_t trace_remaining_longs = BOOTSTRAP_STACK_TRACE_ENTRIES;
+
+static void *trace_slabs[LOCKDEP_MAX_SLABS];
+static unsigned int nr_trace_slabs;
+
 static struct hlist_head stack_trace_hash[STACK_TRACE_HASH_SIZE];
 
-static bool traces_identical(struct lock_trace *t1, struct lock_trace *t2)
+struct lockdep_selftest_snap {
+	unsigned int nr_trace_slabs;
+	unsigned long *trace_free_ptr;
+	size_t trace_remaining_longs;
+	unsigned long nr_trace_entries;
+};
+static struct lockdep_selftest_snap selftest_snap;
+
+static void lockdep_selftest_trace_start(void)
+{
+	selftest_snap.nr_trace_slabs = nr_trace_slabs;
+	selftest_snap.trace_free_ptr = trace_free_ptr;
+	selftest_snap.trace_remaining_longs = trace_remaining_longs;
+	selftest_snap.nr_trace_entries = nr_stack_trace_entries;
+
+	lockdep_report_stage("selftest", "pre-test");
+}
+
+static void lockdep_selftest_trace_finish(void)
+{
+	unsigned int reclaimed_slabs = 0;
+	unsigned int i;
+
+	if (!debug_locks || !selftest_snap.trace_free_ptr)
+		return;
+
+	lockdep_report_stage("selftest", "peak-test");
+
+	for (i = selftest_snap.nr_trace_slabs; i < nr_trace_slabs; i++) {
+		lockdep_release_slab(trace_slabs[i], &ld_slabs.stack_traces);
+		trace_slabs[i] = NULL;
+		reclaimed_slabs++;
+	}
+	nr_trace_slabs = selftest_snap.nr_trace_slabs;
+	trace_free_ptr = selftest_snap.trace_free_ptr;
+	trace_remaining_longs = selftest_snap.trace_remaining_longs;
+	nr_stack_trace_entries = selftest_snap.nr_trace_entries;
+	memset(stack_trace_hash, 0, sizeof(stack_trace_hash));
+
+	if (reclaimed_slabs)
+		pr_info("lockdep: selftest complete : recycled %u trace slabs (%u kB) to pool\n",
+			reclaimed_slabs, (reclaimed_slabs * LOCKDEP_SLAB_SIZE) / 1024);
+
+	lockdep_report_stage("selftest", "post-test");
+}
+
+static inline void lock_trace_discard(struct lock_trace *trace, unsigned int max_entries)
+{
+	size_t needed_longs = LOCK_TRACE_SIZE_IN_LONGS + max_entries;
+
+	if ((unsigned long *)trace + needed_longs == trace_free_ptr) {
+		trace_free_ptr = (unsigned long *)trace;
+		trace_remaining_longs += needed_longs;
+	}
+}
+
+static inline void lock_trace_trim(struct lock_trace *trace, unsigned int unused_entries)
 {
-	return t1->hash == t2->hash && t1->nr_entries == t2->nr_entries &&
-		memcmp(t1->entries, t2->entries,
-		       t1->nr_entries * sizeof(t1->entries[0])) == 0;
+	if (unused_entries && trace_free_ptr) {
+		trace_free_ptr -= unused_entries;
+		trace_remaining_longs += unused_entries;
+	}
 }
 
 static struct lock_trace *save_trace(void)
@@ -648,40 +747,59 @@ static struct lock_trace *save_trace(void)
 	struct lock_trace *trace, *t2;
 	struct hlist_head *hash_head;
 	u32 hash;
-	int max_entries;
+	size_t needed_longs = LOCK_TRACE_SIZE_IN_LONGS + MAX_LOCKDEP_TRACE_DEPTH;
 
 	BUILD_BUG_ON_NOT_POWER_OF_2(STACK_TRACE_HASH_SIZE);
-	BUILD_BUG_ON(LOCK_TRACE_SIZE_IN_LONGS >= MAX_STACK_TRACE_ENTRIES);
-
-	trace = (struct lock_trace *)(stack_trace + nr_stack_trace_entries);
-	max_entries = MAX_STACK_TRACE_ENTRIES - nr_stack_trace_entries -
-		LOCK_TRACE_SIZE_IN_LONGS;
 
-	if (max_entries <= 0) {
-		if (!debug_locks_off_graph_unlock())
-			return NULL;
+	if (trace_remaining_longs < needed_longs) {
+		unsigned long *slab = lockdep_claim_slab(&ld_slabs.stack_traces);
 
-		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_STACK_TRACE_ENTRIES too low!");
-		dump_stack();
-		nbcon_cpu_emergency_exit();
+		if (unlikely(!slab))
+			goto out_fail;
 
-		return NULL;
+		trace_slabs[nr_trace_slabs++] = slab;
+		trace_free_ptr = slab;
+		trace_remaining_longs = LOCKDEP_SLAB_SIZE / sizeof(unsigned long);
 	}
-	trace->nr_entries = stack_trace_save(trace->entries, max_entries, 3);
 
-	hash = jhash(trace->entries, trace->nr_entries *
-		     sizeof(trace->entries[0]), 0);
+	trace = (struct lock_trace *)trace_free_ptr;
+	trace_free_ptr += needed_longs;
+	trace_remaining_longs -= needed_longs;
+
+	trace->nr_entries = stack_trace_save(trace->entries, MAX_LOCKDEP_TRACE_DEPTH, 3);
+	hash = jhash(trace->entries, trace->nr_entries * sizeof(unsigned long), 0);
 	trace->hash = hash;
 	hash_head = stack_trace_hash + (hash & (STACK_TRACE_HASH_SIZE - 1));
+
 	hlist_for_each_entry(t2, hash_head, hash_entry) {
-		if (traces_identical(trace, t2))
+		if (t2->hash == hash && t2->nr_entries == trace->nr_entries &&
+		    !memcmp(t2->entries, trace->entries,
+			    trace->nr_entries * sizeof(unsigned long))) {
+			/* Duplicate hit: rewind speculative allocation */
+			lock_trace_discard(trace, MAX_LOCKDEP_TRACE_DEPTH);
 			return t2;
+		}
 	}
-	nr_stack_trace_entries += LOCK_TRACE_SIZE_IN_LONGS + trace->nr_entries;
+
+	/* Novel trace: trim unused tail frames */
+	if (trace->nr_entries < MAX_LOCKDEP_TRACE_DEPTH)
+		lock_trace_trim(trace, MAX_LOCKDEP_TRACE_DEPTH - trace->nr_entries);
+
 	hlist_add_head(&trace->hash_entry, hash_head);
+	nr_stack_trace_entries += LOCK_TRACE_SIZE_IN_LONGS + trace->nr_entries;
 
 	return trace;
+
+out_fail:
+	if (!debug_locks_off_graph_unlock())
+		return NULL;
+
+	nbcon_cpu_emergency_enter();
+	lockdep_print_watermarks("BUG: lockdep stack trace allocation failed!");
+	dump_stack();
+	nbcon_cpu_emergency_exit();
+
+	return NULL;
 }
 
 /* Return the number of stack traces in the stack_trace[] array. */
@@ -1075,46 +1193,15 @@ static bool assign_lock_key(struct lockdep_map *lock)
 
 #ifdef CONFIG_DEBUG_LOCKDEP
 
-/* Check whether element @e occurs in list @h */
-static bool in_list(struct list_head *e, struct list_head *h)
-{
-	struct list_head *f;
-
-	list_for_each(f, h) {
-		if (e == f)
-			return true;
-	}
-
-	return false;
-}
-
-/*
- * Check whether entry @e occurs in any of the locks_after or locks_before
- * lists.
- */
-static bool in_any_class_list(struct list_head *e)
-{
-	struct lock_class *class;
-	int i;
-
-	for (i = 0; i < ARRAY_SIZE(lock_classes); i++) {
-		class = &lock_classes[i];
-		if (in_list(e, &class->locks_after) ||
-		    in_list(e, &class->locks_before))
-			return true;
-	}
-	return false;
-}
-
 static bool class_lock_list_valid(struct lock_class *c, struct list_head *h)
 {
 	struct lock_list *e;
 
 	list_for_each_entry(e, h, entry) {
 		if (e->links_to != c) {
-			printk(KERN_INFO "class %s: mismatch for lock entry %ld; class %s <> %s",
+			pr_info("class %s: mismatch for lock entry %p; class %s <> %s",
 			       c->name ? : "(?)",
-			       (unsigned long)(e - list_entries),
+			       e,
 			       e->links_to && e->links_to->name ?
 			       e->links_to->name : "(?)",
 			       e->class && e->class->name ? e->class->name :
@@ -1126,7 +1213,8 @@ static bool class_lock_list_valid(struct lock_class *c, struct list_head *h)
 }
 
 #ifdef CONFIG_PROVE_LOCKING
-static u16 chain_hlocks[MAX_LOCKDEP_CHAIN_HLOCKS];
+static u16 get_chain_hlock(unsigned int offset);
+static void set_chain_hlock(unsigned int offset, u16 val);
 #endif
 
 static bool check_lock_chain_key(struct lock_chain *chain)
@@ -1136,14 +1224,14 @@ static bool check_lock_chain_key(struct lock_chain *chain)
 	int i;
 
 	for (i = chain->base; i < chain->base + chain->depth; i++)
-		chain_key = iterate_chain_key(chain_key, chain_hlocks[i]);
+		chain_key = iterate_chain_key(chain_key, get_chain_hlock(i));
 	/*
 	 * The 'unsigned long long' casts avoid that a compiler warning
 	 * is reported when building tools/lib/lockdep.
 	 */
 	if (chain->chain_key != chain_key) {
 		printk(KERN_INFO "chain %lld: key %#llx <> %#llx\n",
-		       (unsigned long long)(chain - lock_chains),
+		       (unsigned long long)chain->chain_idx,
 		       (unsigned long long)chain->chain_key,
 		       (unsigned long long)chain_key);
 		return false;
@@ -1152,42 +1240,15 @@ static bool check_lock_chain_key(struct lock_chain *chain)
 	return true;
 }
 
-static bool in_any_zapped_class_list(struct lock_class *class)
-{
-	struct pending_free *pf;
-	int i;
-
-	for (i = 0, pf = delayed_free.pf; i < ARRAY_SIZE(delayed_free.pf); i++, pf++) {
-		if (in_list(&class->lock_entry, &pf->zapped))
-			return true;
-	}
-
-	return false;
-}
-
 static bool __check_data_structures(void)
 {
 	struct lock_class *class;
 	struct lock_chain *chain;
 	struct hlist_head *head;
-	struct lock_list *e;
 	int i;
 
-	/* Check whether all classes occur in a lock list. */
-	for (i = 0; i < ARRAY_SIZE(lock_classes); i++) {
-		class = &lock_classes[i];
-		if (!in_list(&class->lock_entry, &all_lock_classes) &&
-		    !in_list(&class->lock_entry, &free_lock_classes) &&
-		    !in_any_zapped_class_list(class)) {
-			printk(KERN_INFO "class %px/%s is not in any class list\n",
-			       class, class->name ? : "(?)");
-			return false;
-		}
-	}
-
 	/* Check whether all classes have valid lock lists. */
-	for (i = 0; i < ARRAY_SIZE(lock_classes); i++) {
-		class = &lock_classes[i];
+	list_for_each_entry(class, &all_lock_classes, lock_entry) {
 		if (!class_lock_list_valid(class, &class->locks_before))
 			return false;
 		if (!class_lock_list_valid(class, &class->locks_after))
@@ -1203,38 +1264,6 @@ static bool __check_data_structures(void)
 		}
 	}
 
-	/*
-	 * Check whether all list entries that are in use occur in a class
-	 * lock list.
-	 */
-	for_each_set_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (!in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d is not in any class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class->name ? : "(?)",
-			       e->links_to->name ? : "(?)");
-			return false;
-		}
-	}
-
-	/*
-	 * Check whether all list entries that are not in use do not occur in
-	 * a class lock list.
-	 */
-	for_each_clear_bit(i, list_entries_in_use, ARRAY_SIZE(list_entries)) {
-		e = list_entries + i;
-		if (in_any_class_list(&e->entry)) {
-			printk(KERN_INFO "list entry %d occurs in a class list; class %s <> %s\n",
-			       (unsigned int)(e - list_entries),
-			       e->class && e->class->name ? e->class->name :
-			       "(?)",
-			       e->links_to && e->links_to->name ?
-			       e->links_to->name : "(?)");
-			return false;
-		}
-	}
-
 	return true;
 }
 
@@ -1286,11 +1315,15 @@ static void init_data_structures_once(void)
 	INIT_LIST_HEAD(&delayed_free.pf[0].zapped);
 	INIT_LIST_HEAD(&delayed_free.pf[1].zapped);
 
-	for (i = 0; i < ARRAY_SIZE(lock_classes); i++) {
-		list_add_tail(&lock_classes[i].lock_entry, &free_lock_classes);
-		INIT_LIST_HEAD(&lock_classes[i].locks_after);
-		INIT_LIST_HEAD(&lock_classes[i].locks_before);
+	for (i = 0; i < lock_class_PER_CHUNK; i++) {
+		struct lock_class *class = &lock_class_chunk0[i];
+
+		class->class_idx = i;
+		list_add_tail(&class->lock_entry, &free_lock_classes);
+		INIT_LIST_HEAD(&class->locks_after);
+		INIT_LIST_HEAD(&class->locks_before);
 	}
+
 	init_chain_block_buckets();
 }
 
@@ -1371,7 +1404,7 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
 	struct lockdep_subclass_key *key;
 	struct hlist_head *hash_head;
 	struct lock_class *class;
-	int idx;
+	int idx, i;
 
 	DEBUG_LOCKS_WARN_ON(!irqs_disabled());
 
@@ -1406,19 +1439,43 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
 	/* Allocate a new lock class and add it to the hash. */
 	class = list_first_entry_or_null(&free_lock_classes, typeof(*class),
 					 lock_entry);
+	if (!class) {
+		if (nr_lock_class_chunks < LOCKDEP_MAX_SLABS) {
+			struct lock_class *chunk;
+			unsigned int chunk_idx = nr_lock_class_chunks;
+
+			chunk = lockdep_claim_slab(&ld_slabs.lock_classes);
+			if (chunk) {
+				memset(chunk, 0, sizeof(struct lock_class) * lock_class_PER_CHUNK);
+				for (i = 0; i < lock_class_PER_CHUNK; i++) {
+					struct lock_class *c = &chunk[i];
+
+					c->class_idx = chunk_idx * lock_class_PER_CHUNK + i;
+					INIT_LIST_HEAD(&c->locks_after);
+					INIT_LIST_HEAD(&c->locks_before);
+					list_add_tail(&c->lock_entry, &free_lock_classes);
+				}
+				/* Pairs with smp_load_acquire() in idx_to_lock_class() */
+				smp_store_release(&lock_class_chunks[chunk_idx], chunk);
+				nr_lock_class_chunks++;
+				class = list_first_entry_or_null(&free_lock_classes, typeof(*class),
+								 lock_entry);
+			}
+		}
+	}
 	if (!class) {
 		if (!debug_locks_off_graph_unlock()) {
 			return NULL;
 		}
 
 		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_KEYS too low!");
+		lockdep_print_watermarks("BUG: MAX_LOCKDEP_KEYS too low!");
 		dump_stack();
 		nbcon_cpu_emergency_exit();
 		return NULL;
 	}
 	nr_lock_classes++;
-	__set_bit(class - lock_classes, lock_classes_in_use);
+	__set_bit(class->class_idx, lock_classes_in_use);
 	debug_atomic_inc(nr_unused_locks);
 	class->key = key;
 	class->name = lock->name;
@@ -1439,7 +1496,7 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
 	 * of classes.
 	 */
 	list_move_tail(&class->lock_entry, &all_lock_classes);
-	idx = class - lock_classes;
+	idx = class->class_idx;
 	if (idx > max_lock_class_idx)
 		max_lock_class_idx = idx;
 
@@ -1484,22 +1541,39 @@ register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
  */
 static struct lock_list *alloc_list_entry(void)
 {
-	int idx = find_first_zero_bit(list_entries_in_use,
-				      ARRAY_SIZE(list_entries));
+	struct lock_list *entry;
 
-	if (idx >= ARRAY_SIZE(list_entries)) {
-		if (!debug_locks_off_graph_unlock())
-			return NULL;
+	if (list_entries_freelist) {
+		entry = list_entries_freelist;
+		list_entries_freelist = *(void **)entry;
+	} else if (list_entries_remaining > 0) {
+		entry = list_entries_cur++;
+		list_entries_remaining--;
+	} else {
+		struct lock_list *slab = lockdep_claim_slab(&ld_slabs.direct_deps);
 
-		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_ENTRIES too low!");
-		dump_stack();
-		nbcon_cpu_emergency_exit();
-		return NULL;
+		if (unlikely(!slab))
+			goto out_fail;
+
+		list_entries_cur = slab;
+		list_entries_remaining = LOCKDEP_SLAB_SIZE / sizeof(struct lock_list);
+		entry = list_entries_cur++;
+		list_entries_remaining--;
 	}
+
+	memset(entry, 0, sizeof(*entry));
 	nr_list_entries++;
-	__set_bit(idx, list_entries_in_use);
-	return list_entries + idx;
+	return entry;
+
+out_fail:
+	if (!debug_locks_off_graph_unlock())
+		return NULL;
+
+	nbcon_cpu_emergency_enter();
+	print_lockdep_off("BUG: lockdep pool exhausted!");
+	dump_stack();
+	nbcon_cpu_emergency_exit();
+	return NULL;
 }
 
 /*
@@ -3407,9 +3481,32 @@ check_prevs_add(struct task_struct *curr, struct held_lock *next)
 	return 0;
 }
 
-struct lock_chain lock_chains[MAX_LOCKDEP_CHAINS];
+DEFINE_CHUNKED_ARRAY(lock_chain, struct lock_chain);
 static DECLARE_BITMAP(lock_chains_in_use, MAX_LOCKDEP_CHAINS);
-static u16 chain_hlocks[MAX_LOCKDEP_CHAIN_HLOCKS];
+
+DEFINE_CHUNKED_ARRAY(chain_hlock, u16);
+static unsigned int total_chain_hlocks_capacity = chain_hlock_PER_CHUNK;
+
+unsigned int chain_hlocks_used(void)
+{
+	return total_chain_hlocks_capacity - (nr_free_chain_hlocks + nr_lost_chain_hlocks);
+}
+
+static inline u16 get_chain_hlock(unsigned int offset)
+{
+	u16 *p = idx_to_chain_hlock(offset);
+
+	return p ? *p : 0;
+}
+
+static inline void set_chain_hlock(unsigned int offset, u16 val)
+{
+	u16 *p = idx_to_chain_hlock(offset);
+
+	if (p)
+		*p = val;
+}
+
 unsigned long nr_zapped_lock_chains;
 unsigned int nr_free_chain_hlocks;	/* Free chain_hlocks in buckets */
 unsigned int nr_lost_chain_hlocks;	/* Lost chain_hlocks */
@@ -3459,7 +3556,7 @@ static inline int size_to_bucket(int size)
  */
 static inline int chain_block_next(int offset)
 {
-	int next = chain_hlocks[offset];
+	int next = get_chain_hlock(offset);
 
 	WARN_ON_ONCE(!(next & CHAIN_BLK_FLAG));
 
@@ -3468,7 +3565,7 @@ static inline int chain_block_next(int offset)
 
 	next &= ~CHAIN_BLK_FLAG;
 	next <<= 16;
-	next |= chain_hlocks[offset + 1];
+	next |= get_chain_hlock(offset + 1);
 
 	return next;
 }
@@ -3478,17 +3575,17 @@ static inline int chain_block_next(int offset)
  */
 static inline int chain_block_size(int offset)
 {
-	return (chain_hlocks[offset + 2] << 16) | chain_hlocks[offset + 3];
+	return (get_chain_hlock(offset + 2) << 16) | get_chain_hlock(offset + 3);
 }
 
 static inline void init_chain_block(int offset, int next, int bucket, int size)
 {
-	chain_hlocks[offset] = (next >> 16) | CHAIN_BLK_FLAG;
-	chain_hlocks[offset + 1] = (u16)next;
+	set_chain_hlock(offset, (next >> 16) | CHAIN_BLK_FLAG);
+	set_chain_hlock(offset + 1, (u16)next);
 
 	if (size && !bucket) {
-		chain_hlocks[offset + 2] = size >> 16;
-		chain_hlocks[offset + 3] = (u16)size;
+		set_chain_hlock(offset + 2, size >> 16);
+		set_chain_hlock(offset + 3, (u16)size);
 	}
 }
 
@@ -3563,7 +3660,7 @@ static void init_chain_block_buckets(void)
 	for (i = 0; i < MAX_CHAIN_BUCKETS; i++)
 		chain_block_buckets[i] = -1;
 
-	add_chain_block(0, ARRAY_SIZE(chain_hlocks));
+	add_chain_block(0, chain_hlock_PER_CHUNK);
 }
 
 /*
@@ -3584,14 +3681,33 @@ static int alloc_chain_hlocks(int req)
 
 	init_data_structures_once();
 
-	if (nr_free_chain_hlocks < req)
-		return -1;
-
 	/*
 	 * We require a minimum of 2 (u16) entries to encode a freelist
 	 * 'pointer'.
 	 */
 	req = max(req, 2);
+
+retry:
+	if (nr_free_chain_hlocks < req) {
+		if (nr_chain_hlock_chunks < LOCKDEP_MAX_SLABS) {
+			unsigned int chunk_idx = nr_chain_hlock_chunks;
+			unsigned int base_offset = chunk_idx * chain_hlock_PER_CHUNK;
+			u16 *chunk;
+
+			chunk = lockdep_claim_slab(&ld_slabs.chain_hlocks);
+			if (chunk) {
+				memset(chunk, 0, sizeof(u16) * chain_hlock_PER_CHUNK);
+				/* Pairs with smp_load_acquire() in idx_to_chain_hlock() */
+				smp_store_release(&chain_hlock_chunks[chunk_idx], chunk);
+				nr_chain_hlock_chunks++;
+				total_chain_hlocks_capacity += chain_hlock_PER_CHUNK;
+				add_chain_block(base_offset, chain_hlock_PER_CHUNK);
+			}
+		}
+		if (nr_free_chain_hlocks < req)
+			return -1;
+	}
+
 	bucket = size_to_bucket(req);
 	curr = chain_block_buckets[bucket];
 
@@ -3632,6 +3748,24 @@ static int alloc_chain_hlocks(int req)
 		return curr;
 	}
 
+	/* If fragmented and chunks remain, expand with a new chunk */
+	if (nr_chain_hlock_chunks < LOCKDEP_MAX_SLABS) {
+		unsigned int chunk_idx = nr_chain_hlock_chunks;
+		unsigned int base_offset = chunk_idx * chain_hlock_PER_CHUNK;
+		u16 *chunk;
+
+		chunk = lockdep_claim_slab(&ld_slabs.chain_hlocks);
+		if (chunk) {
+			memset(chunk, 0, sizeof(u16) * chain_hlock_PER_CHUNK);
+			/* Pairs with smp_load_acquire() in idx_to_chain_hlock() */
+			smp_store_release(&chain_hlock_chunks[chunk_idx], chunk);
+			nr_chain_hlock_chunks++;
+			total_chain_hlocks_capacity += chain_hlock_PER_CHUNK;
+			add_chain_block(base_offset, chain_hlock_PER_CHUNK);
+			goto retry;
+		}
+	}
+
 	return -1;
 }
 
@@ -3642,10 +3776,10 @@ static inline void free_chain_hlocks(int base, int size)
 
 struct lock_class *lock_chain_get_class(struct lock_chain *chain, int i)
 {
-	u16 chain_hlock = chain_hlocks[chain->base + i];
+	u16 chain_hlock = get_chain_hlock(chain->base + i);
 	unsigned int class_idx = chain_hlock_class_idx(chain_hlock);
 
-	return lock_classes + class_idx;
+	return idx_to_lock_class(class_idx);
 }
 
 /*
@@ -3710,10 +3844,10 @@ static void print_chain_keys_chain(struct lock_chain *chain)
 
 	printk("depth: %u\n", chain->depth);
 	for (i = 0; i < chain->depth; i++) {
-		hlock_id = chain_hlocks[chain->base + i];
+		hlock_id = get_chain_hlock(chain->base + i);
 		chain_key = print_chain_key_iteration(hlock_id, chain_key);
 
-		print_lock_name(NULL, lock_classes + chain_hlock_class_idx(hlock_id));
+		print_lock_name(NULL, idx_to_lock_class(chain_hlock_class_idx(hlock_id)));
 		printk("\n");
 	}
 }
@@ -3768,7 +3902,7 @@ static int check_no_collision(struct task_struct *curr,
 	for (j = 0; j < chain->depth - 1; j++, i++) {
 		id = hlock_id(&curr->held_locks[i]);
 
-		if (DEBUG_LOCKS_WARN_ON(chain_hlocks[chain->base + j] != id)) {
+		if (DEBUG_LOCKS_WARN_ON(get_chain_hlock(chain->base + j) != id)) {
 			print_collision(curr, hlock, chain);
 			return 0;
 		}
@@ -3783,25 +3917,64 @@ static int check_no_collision(struct task_struct *curr,
  */
 long lockdep_next_lockchain(long i)
 {
-	i = find_next_bit(lock_chains_in_use, ARRAY_SIZE(lock_chains), i + 1);
-	return i < ARRAY_SIZE(lock_chains) ? i : -2;
+	i = find_next_bit(lock_chains_in_use, MAX_LOCKDEP_CHAINS, i + 1);
+	return i < MAX_LOCKDEP_CHAINS ? i : -2;
 }
 
 unsigned long lock_chain_count(void)
 {
-	return bitmap_weight(lock_chains_in_use, ARRAY_SIZE(lock_chains));
+	return bitmap_weight(lock_chains_in_use, MAX_LOCKDEP_CHAINS);
+}
+
+static void lockdep_print_watermarks(const char *bug_msg)
+{
+	print_lockdep_off(bug_msg);
+	pr_err("Lockdep Stats: classes=%lu (chunks=%u), entries=%lu, chains=%lu (chunks=%u), hlocks=%u (chunks=%u)\n",
+	       nr_lock_classes, nr_lock_class_chunks,
+	       nr_list_entries,
+	       lock_chain_count(), nr_lock_chain_chunks,
+	       chain_hlocks_used(), nr_chain_hlock_chunks);
+	pr_err("Lockdep Slabs: total=%u, used=%u (classes=%u, entries=%u, chains=%u, hlocks=%u, trace=%u), free=%u\n",
+	       lockdep_nr_slabs, lockdep_slabs_used,
+	       ld_slabs.lock_classes, ld_slabs.direct_deps,
+	       ld_slabs.lock_chains, ld_slabs.chain_hlocks,
+	       ld_slabs.stack_traces,
+	       lockdep_nr_slabs > lockdep_slabs_used ? lockdep_nr_slabs - lockdep_slabs_used : 0);
+	show_mem();
 }
 
 /* Must be called with the graph lock held. */
 static struct lock_chain *alloc_lock_chain(void)
 {
-	int idx = find_first_zero_bit(lock_chains_in_use,
-				      ARRAY_SIZE(lock_chains));
+	int idx = find_first_zero_bit(lock_chains_in_use, MAX_LOCKDEP_CHAINS);
+	unsigned int chunk_idx;
+	struct lock_chain *chain;
 
-	if (unlikely(idx >= ARRAY_SIZE(lock_chains)))
+	if (unlikely(idx >= MAX_LOCKDEP_CHAINS))
 		return NULL;
+
+	chunk_idx = reciprocal_divide(idx, lock_chain_rv);
+	if (chunk_idx >= LOCKDEP_MAX_SLABS)
+		return NULL;
+
+	if (chunk_idx >= nr_lock_chain_chunks) {
+		struct lock_chain *chunk;
+
+		chunk = lockdep_claim_slab(&ld_slabs.lock_chains);
+		if (!chunk)
+			return NULL;
+
+		memset(chunk, 0, sizeof(struct lock_chain) * lock_chain_PER_CHUNK);
+		/* Pairs with smp_load_acquire() in idx_to_lock_chain() */
+		smp_store_release(&lock_chain_chunks[chunk_idx], chunk);
+		nr_lock_chain_chunks = chunk_idx + 1;
+	}
+
 	__set_bit(idx, lock_chains_in_use);
-	return lock_chains + idx;
+	chain = idx_to_lock_chain(idx);
+	memset(chain, 0, sizeof(*chain));
+	chain->chain_idx = idx;
+	return chain;
 }
 
 /*
@@ -3833,7 +4006,7 @@ static inline int add_chain_cache(struct task_struct *curr,
 			return 0;
 
 		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_CHAINS too low!");
+		lockdep_print_watermarks("BUG: MAX_LOCKDEP_CHAINS too low!");
 		dump_stack();
 		nbcon_cpu_emergency_exit();
 		return 0;
@@ -3843,9 +4016,9 @@ static inline int add_chain_cache(struct task_struct *curr,
 	i = get_first_held_lock(curr, hlock);
 	chain->depth = curr->lockdep_depth + 1 - i;
 
-	BUILD_BUG_ON((1UL << 24) <= ARRAY_SIZE(chain_hlocks));
+	BUILD_BUG_ON((1UL << 24) <= MAX_LOCKDEP_CHAIN_HLOCKS);
 	BUILD_BUG_ON((1UL << 6)  <= ARRAY_SIZE(curr->held_locks));
-	BUILD_BUG_ON((1UL << 8*sizeof(chain_hlocks[0])) <= ARRAY_SIZE(lock_classes));
+	BUILD_BUG_ON((1UL << (8 * sizeof(u16))) <= MAX_LOCKDEP_KEYS);
 
 	j = alloc_chain_hlocks(chain->depth);
 	if (j < 0) {
@@ -3853,7 +4026,7 @@ static inline int add_chain_cache(struct task_struct *curr,
 			return 0;
 
 		nbcon_cpu_emergency_enter();
-		print_lockdep_off("BUG: MAX_LOCKDEP_CHAIN_HLOCKS too low!");
+		lockdep_print_watermarks("BUG: MAX_LOCKDEP_CHAIN_HLOCKS too low!");
 		dump_stack();
 		nbcon_cpu_emergency_exit();
 		return 0;
@@ -3863,9 +4036,9 @@ static inline int add_chain_cache(struct task_struct *curr,
 	for (j = 0; j < chain->depth - 1; j++, i++) {
 		int lock_id = hlock_id(curr->held_locks + i);
 
-		chain_hlocks[chain->base + j] = lock_id;
+		set_chain_hlock(chain->base + j, lock_id);
 	}
-	chain_hlocks[chain->base + j] = hlock_id(hlock);
+	set_chain_hlock(chain->base + j, hlock_id(hlock));
 	hlist_add_head_rcu(&chain->entry, hash_head);
 	debug_atomic_inc(chain_lookup_misses);
 	inc_chains(chain->irq_context);
@@ -5222,7 +5395,7 @@ static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass,
 	if (DEBUG_LOCKS_WARN_ON(depth >= MAX_LOCK_DEPTH))
 		return 0;
 
-	class_idx = class - lock_classes;
+	class_idx = class->class_idx;
 
 	if (depth && !sync) {
 		/* we're holding locks and the new held lock is not a sync */
@@ -5413,7 +5586,7 @@ static noinstr int match_held_lock(const struct held_lock *hlock,
 		if (DEBUG_LOCKS_WARN_ON(!hlock->nest_lock))
 			return 0;
 
-		if (hlock->class_idx == class - lock_classes)
+		if (hlock->class_idx == class->class_idx)
 			return 1;
 	}
 
@@ -5521,7 +5694,7 @@ __lock_set_class(struct lockdep_map *lock, const char *name,
 			      lock->wait_type_outer,
 			      lock->lock_type);
 	class = register_lock_class(lock, subclass, 0);
-	hlock->class_idx = class - lock_classes;
+	hlock->class_idx = class->class_idx;
 
 	curr->lockdep_depth = i;
 	curr->curr_chain_key = hlock->prev_chain_key;
@@ -5871,7 +6044,7 @@ static void verify_lock_unused(struct lockdep_map *lock, struct held_lock *hlock
 	if (!(class->usage_mask & mask))
 		return;
 
-	hlock->class_idx = class - lock_classes;
+	hlock->class_idx = class->class_idx;
 
 	print_usage_bug(current, hlock, LOCK_USED, LOCK_USAGE_STATES);
 #endif
@@ -6279,7 +6452,7 @@ static void remove_class_from_lock_chain(struct pending_free *pf,
 	int i;
 
 	for (i = chain->base; i < chain->base + chain->depth; i++) {
-		if (chain_hlock_class_idx(chain_hlocks[i]) != class - lock_classes)
+		if (chain_hlock_class_idx(get_chain_hlock(i)) != class->class_idx)
 			continue;
 		/*
 		 * Each lock class occurs at most once in a lock chain so once
@@ -6301,7 +6474,7 @@ static void remove_class_from_lock_chain(struct pending_free *pf,
 	 * hlist_for_each_entry_rcu() loop is safe.
 	 */
 	hlist_del_rcu(&chain->entry);
-	__set_bit(chain - lock_chains, pf->lock_chains_being_freed);
+	__set_bit(chain->chain_idx, pf->lock_chains_being_freed);
 	nr_zapped_lock_chains++;
 #endif
 }
@@ -6338,28 +6511,28 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 	list_for_each_entry_safe(entry, tmp, &class->locks_after, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_before, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
+				free_list_entry(other);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
+		free_list_entry(entry);
 	}
 	list_for_each_entry_safe(entry, tmp, &class->locks_before, entry) {
 		list_for_each_entry_safe(other, other_tmp, &entry->links_to->locks_after, entry) {
 			if (other->links_to == class) {
-				__clear_bit(other - list_entries, list_entries_in_use);
 				nr_list_entries--;
 				list_del_rcu(&other->entry);
+				free_list_entry(other);
 				break;
 			}
 		}
-		__clear_bit(entry - list_entries, list_entries_in_use);
 		nr_list_entries--;
 		list_del_rcu(&entry->entry);
+		free_list_entry(entry);
 	}
 	if (list_empty(&class->locks_after) &&
 	    list_empty(&class->locks_before)) {
@@ -6371,8 +6544,8 @@ static void zap_class(struct pending_free *pf, struct lock_class *class)
 		if (class->usage_mask == 0)
 			debug_atomic_dec(nr_unused_locks);
 		nr_lock_classes--;
-		__clear_bit(class - lock_classes, lock_classes_in_use);
-		if (class - lock_classes == max_lock_class_idx)
+		__clear_bit(class->class_idx, lock_classes_in_use);
+		if (class->class_idx == max_lock_class_idx)
 			max_lock_class_idx--;
 	} else {
 		WARN_ONCE(true, "%s() failed for class %s\n", __func__,
@@ -6450,8 +6623,8 @@ static void __free_zapped_classes(struct pending_free *pf)
 
 #ifdef CONFIG_PROVE_LOCKING
 	bitmap_andnot(lock_chains_in_use, lock_chains_in_use,
-		      pf->lock_chains_being_freed, ARRAY_SIZE(lock_chains));
-	bitmap_clear(pf->lock_chains_being_freed, 0, ARRAY_SIZE(lock_chains));
+		      pf->lock_chains_being_freed, MAX_LOCKDEP_CHAINS);
+	bitmap_clear(pf->lock_chains_being_freed, 0, MAX_LOCKDEP_CHAINS);
 #endif
 }
 
@@ -6773,12 +6946,16 @@ void __init lockdep_early_init(void)
 		lockdep_slabs[i] = (char *)pool + (i * LOCKDEP_SLAB_SIZE);
 
 	lockdep_nr_slabs = nr_slabs;
-	pr_info("lockdep: reserved %u slabs (%zu KB) from memblock\n",
+	lockdep_slabs_used = 0;
+
+	pr_info("lockdep: reserved %u slabs (%zu KB) from memblock for dynamic tables\n",
 		nr_slabs, slab_bytes / 1024);
 }
 
 void __init lockdep_init(void)
 {
+	init_data_structures_once();
+
 	pr_info("Lock dependency validator: Copyright (c) 2006 Red Hat, Inc., Ingo Molnar\n");
 
 	pr_info("... MAX_LOCKDEP_SUBCLASSES:  %lu\n", MAX_LOCKDEP_SUBCLASSES);
@@ -6789,26 +6966,25 @@ void __init lockdep_init(void)
 	pr_info("... MAX_LOCKDEP_CHAINS:      %lu\n", MAX_LOCKDEP_CHAINS);
 	pr_info("... CHAINHASH_SIZE:          %lu\n", CHAINHASH_SIZE);
 
-	pr_info(" memory used by lock dependency info: %zu kB\n",
-	       (sizeof(lock_classes) +
+	pr_info(" memory used by lock dependency info: dynamic (bootstrap %zu kB)\n",
+	       (sizeof(lock_class_chunk0) +
 		sizeof(lock_classes_in_use) +
 		sizeof(classhash_table) +
 		sizeof(list_entries) +
-		sizeof(list_entries_in_use) +
 		sizeof(chainhash_table) +
 		sizeof(delayed_free)
 #ifdef CONFIG_PROVE_LOCKING
 		+ sizeof(lock_cq)
-		+ sizeof(lock_chains)
+		+ sizeof(lock_chain_chunk0)
 		+ sizeof(lock_chains_in_use)
-		+ sizeof(chain_hlocks)
+		+ sizeof(chain_hlock_chunk0)
 #endif
 		) / 1024
 		);
 
 #if defined(CONFIG_TRACE_IRQFLAGS) && defined(CONFIG_PROVE_LOCKING)
-	pr_info(" memory used for stack traces: %zu kB\n",
-	       (sizeof(stack_trace) + sizeof(stack_trace_hash)) / 1024
+	pr_info(" memory used for stack traces: dynamic (bootstrap %zu kB)\n",
+	       sizeof(stack_trace) / 1024
 	       );
 #endif
 
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index 3344361a1c3b..eaa23d9b4dd5 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -7,6 +7,15 @@
  * lockdep subsystem internal functions and variables.
  */
 
+#include <linux/types.h>
+#include <linux/reciprocal_div.h>
+#include <linux/log2.h>
+#include <asm/barrier.h>
+
+#define LOCKDEP_SLAB_SIZE	(64 * 1024)
+#define LOCKDEP_MAX_SLABS	512
+#define LOCKDEP_DEFAULT_SLABS	64
+
 /*
  * Lock-class usage-state bits:
  */
@@ -122,19 +131,25 @@ enum {
 #define MAX_LOCKDEP_CHAINS	(1UL << MAX_LOCKDEP_CHAINS_BITS)
 
 #define AVG_LOCKDEP_CHAIN_DEPTH		5
-#include <linux/reciprocal_div.h>
+#define MAX_LOCKDEP_CHAIN_HLOCKS (MAX_LOCKDEP_CHAINS * AVG_LOCKDEP_CHAIN_DEPTH)
 
-#define LOCKDEP_SLAB_SIZE	(64 * 1024)
-#define LOCKDEP_MAX_SLABS	512
-#define LOCKDEP_DEFAULT_SLABS	64
+/*
+ * Compile-time precomputation of struct reciprocal_value using the canonical
+ * Granlund-Montgomery algorithm matching lib/math/reciprocal_div.c.
+ */
+#define RECIPROCAL_VALUE_INIT(d) { \
+	.m = (u32)((((1ULL << 32) * ((1ULL << (ilog2((d) - 1) + 1)) - (d))) / (d)) + 1), \
+	.sh1 = (ilog2((d) - 1) + 1) > 0 ? 1 : 0, \
+	.sh2 = (ilog2((d) - 1) + 1) > 1 ? (ilog2((d) - 1) + 1) - 1 : 0, \
+}
 
 /*
  * Chunked Array Tables:
  * Replaces flat monolithic BSS arrays with 2D chunk pointer matrices.
  * Chunk 0 is statically allocated in BSS for early boot, while subsequent
  * chunks are claimed from the memblock reservoir via lockdep_claim_slab().
- * Indexing uses compile-time Granlund-Montgomery reciprocal divide
- * (~3-cycle multiply+shift, zero division instructions).
+ * Indexing uses a compile-time hybrid: single-cycle bit shifts for power-of-2
+ * elements, and Granlund-Montgomery reciprocal divide for non-power-of-2.
  */
 #define DECLARE_CHUNKED_ARRAY(name, type)					\
 	enum {									\
@@ -156,20 +171,16 @@ enum {
 		return &chunk_ptr[offset];					\
 	}
 
-#define DEFINE_CHUNKED_ARRAY(name, type)					\
+#define DEFINE_CHUNKED_ARRAY(name, type)						\
 	static type name##_chunk0[name##_PER_CHUNK];				\
 	type *name##_chunks[LOCKDEP_MAX_SLABS] = { name##_chunk0 };		\
 	static unsigned int nr_##name##_chunks = 1;				\
 	const struct reciprocal_value name##_rv =				\
 		RECIPROCAL_VALUE_INIT(name##_PER_CHUNK)
 
-struct lockdep_slab_usage {
-	unsigned int lock_classes;
-	unsigned int direct_deps;
-	unsigned int lock_chains;
-	unsigned int chain_hlocks;
-	unsigned int stack_traces;
-};
+DECLARE_CHUNKED_ARRAY(lock_chain, struct lock_chain);
+DECLARE_CHUNKED_ARRAY(chain_hlock, u16);
+void lockdep_chain_stats(unsigned int *nr_chunks, size_t *chunk_size, size_t *tail_used);
 
 #define LOCK_USAGE_CHARS (2*XXX_LOCK_USAGE_STATES + 1)
 
@@ -201,9 +212,27 @@ extern unsigned int max_lockdep_depth;
 extern unsigned int max_bfs_queue_depth;
 extern unsigned long max_lock_class_idx;
 
-extern struct lock_class lock_classes[MAX_LOCKDEP_KEYS];
+DECLARE_2D_RADIX(lock_class, struct lock_class);
 extern unsigned long lock_classes_in_use[];
 
+struct lockdep_slab_usage {
+	unsigned int lock_classes;
+	unsigned int direct_deps;
+	unsigned int lock_chains;
+	unsigned int chain_hlocks;
+	unsigned int stack_traces;
+};
+
+struct lockdep_slab_stats {
+	unsigned int total_slabs;
+	unsigned int used_slabs;
+	struct lockdep_slab_usage usage;
+};
+
+unsigned int chain_hlocks_used(void);
+unsigned long lock_chain_count(void);
+void lockdep_get_slab_stats(struct lockdep_slab_stats *st);
+
 #ifdef CONFIG_PROVE_LOCKING
 extern unsigned long lockdep_count_forward_deps(struct lock_class *);
 extern unsigned long lockdep_count_backward_deps(struct lock_class *);
@@ -286,7 +315,7 @@ static inline void debug_class_ops_inc(struct lock_class *class)
 {
 	int idx;
 
-	idx = class - lock_classes;
+	idx = class->class_idx;
 	__debug_atomic_inc(lock_class_ops[idx]);
 }
 
@@ -295,7 +324,7 @@ static inline unsigned long debug_class_ops_read(struct lock_class *class)
 	int idx, cpu;
 	unsigned long ops = 0;
 
-	idx = class - lock_classes;
+	idx = class->class_idx;
 	for_each_possible_cpu(cpu)
 		ops += per_cpu(lockdep_stats.lock_class_ops[idx], cpu);
 	return ops;

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 5/8] lockdep: Fast-path power-of-2 tables with shift/mask indexing
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (3 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 4/8] lockdep: Convert 5 graph arrays to chunked tables backed by slab pool Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 6/8] lockdep: Free unused reservation slabs to buddy allocator at late boot Jim Cromie
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

The 2D chunked arrays (DECLARE_CHUNKED_ARRAY) use Granlund-Montgomery
reciprocal division (reciprocal_divide()) to map indices to (chunk, offset)
tuples across 64 KB slabs. This achieves >99.8% packing density for
non-power-of-2 structs (lock_classes @ 160 B and list_entries @ 48 B).

However, the ultra-hot cache-verification tables (lock_chains @ 32 B and
chain_hlocks @ 2 B) have exact power-of-2 chunk counts (2,048 and 32,768
elements per 64 KB slab).

Add a compile-time branch in DECLARE_CHUNKED_ARRAY() using __builtin_ctz():
for power-of-2 tables, GCC/Clang folds translation into single-cycle bit
shifts (idx >> SHIFT) and masks (idx & MASK), eliminating reciprocal
multiplication overhead entirely from the hot acquire validation path.

Workload Progression (hackbench -p -g 8 -l 1000, 4 vCPUs):

  Metric        Upstream (1D) Generic (P2)      Fast-Path (P3)   Delta
  ====================================================================
  Runtime             8.482 s    8.895 s (+4.8%)       8.278 s  -2.40%
  Cycles          52899510936   55428687460     52033166458     -1.64%
  Instructions    29008189069   31932214532     31698626928     +9.27%

By replacing G-M multiplication with single-cycle bit shifts on the hot
cache verification tables, cycle overhead drops by ~6.4% relative to
Patch 2, bringing total cycles to parity with or slightly faster than
upstream baseline (-1.64% cycles).

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c           |  2 +-
 kernel/locking/lockdep_internals.h | 15 +++++++++++++--
 2 files changed, 14 insertions(+), 3 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 1c8db52af1ac..b2dc7619a5e3 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -3953,7 +3953,7 @@ static struct lock_chain *alloc_lock_chain(void)
 	if (unlikely(idx >= MAX_LOCKDEP_CHAINS))
 		return NULL;
 
-	chunk_idx = reciprocal_divide(idx, lock_chain_rv);
+	chunk_idx = idx / lock_chain_PER_CHUNK;
 	if (chunk_idx >= LOCKDEP_MAX_SLABS)
 		return NULL;
 
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index eaa23d9b4dd5..ccd7343af672 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -154,14 +154,25 @@ enum {
 #define DECLARE_CHUNKED_ARRAY(name, type)					\
 	enum {									\
 		name##_PER_CHUNK = (LOCKDEP_SLAB_SIZE / sizeof(type)),		\
+		name##_IS_P2     = (!(name##_PER_CHUNK & (name##_PER_CHUNK - 1))), \
+		name##_SHIFT     = (__builtin_ctz(name##_PER_CHUNK)),		\
+		name##_MASK      = (name##_PER_CHUNK - 1),			\
 	};									\
 	extern type * name##_chunks[LOCKDEP_MAX_SLABS];				\
 	extern const struct reciprocal_value name##_rv;				\
 	static __always_inline type *idx_to_##name(unsigned int idx)		\
 	{									\
-		unsigned int chunk = reciprocal_divide(idx, name##_rv);		\
-		unsigned int offset = idx - (chunk * name##_PER_CHUNK);		\
+		unsigned int chunk, offset;					\
 		type *chunk_ptr;						\
+										\
+		if (name##_IS_P2) {						\
+			chunk = idx >> name##_SHIFT;				\
+			offset = idx & name##_MASK;				\
+		} else {							\
+			chunk = reciprocal_divide(idx, name##_rv);		\
+			offset = idx - (chunk * name##_PER_CHUNK);		\
+		}								\
+										\
 		if (unlikely(chunk >= LOCKDEP_MAX_SLABS))			\
 			return NULL;						\
 		/* Pairs with smp_store_release() when new chunk slabs are published */ \

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 6/8] lockdep: Free unused reservation slabs to buddy allocator at late boot
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (4 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 5/8] lockdep: Fast-path power-of-2 tables with shift/mask indexing Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 7/8] lockdep: Expose slab pool telemetry in /proc/lockdep_stats and initcalls Jim Cromie
                   ` (2 subsequent siblings)
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

During early boot, lockdep reserves a generous slab pool (e.g. 64 slabs =
4 MB) from memblock to guarantee uninterrupted initialization. Once
the kernel reaches late_initcall, the boot locking storm is complete,
and lockdep's steady-state working set is known.

Add lockdep_post_init_trim() as a late_initcall_sync handler. Calculate
required runtime headroom (default 100% headroom over boot usage, with
a floor of 32 slabs, or custom lockdep_slabs=N / lockdep_headroom=M%),
and return all excess slabs to the buddy page allocator via
free_reserved_page().

Also register a reboot notifier to log total lifetime slab consumption
and remaining headroom on clean system shutdown.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c      | 71 +++++++++++++++++++++++++++++++++++++++++++
 kernel/locking/lockdep_proc.c | 23 +++++++-------
 2 files changed, 83 insertions(+), 11 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index b2dc7619a5e3..3fb6c07cea36 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -6992,6 +6992,77 @@ void __init lockdep_init(void)
 	       sizeof(((struct task_struct *)NULL)->held_locks));
 }
 
+static int lockdep_shutdown_notify(struct notifier_block *nb,
+				   unsigned long code, void *unused)
+{
+	unsigned int used = lockdep_slabs_used - lockdep_nr_free_slabs;
+	unsigned int total = lockdep_nr_slabs;
+	unsigned int free_slabs = total > used ? total - used : 0;
+	unsigned int headroom_pct = used ? (free_slabs * 100) / used : 0;
+
+	pr_info("lockdep: shutdown summary : %u/%u slabs (%u kB/%u kB, %u%% headroom left), %lu classes, %lu chains, %u hlocks\n",
+		used, total,
+		(used * LOCKDEP_SLAB_SIZE) / 1024,
+		(total * LOCKDEP_SLAB_SIZE) / 1024,
+		headroom_pct,
+		nr_lock_classes, lock_chain_count(), chain_hlocks_used());
+
+	return NOTIFY_OK;
+}
+
+static struct notifier_block lockdep_reboot_nb = {
+	.notifier_call = lockdep_shutdown_notify,
+};
+
+static int __init lockdep_post_init_trim(void)
+{
+	unsigned int used = lockdep_slabs_used - lockdep_nr_free_slabs;
+	unsigned int initial_slabs = lockdep_nr_slabs;
+	unsigned int target_slabs, kept_headroom_slabs;
+	unsigned int freed_slabs = 0;
+	unsigned int i;
+
+	if (!lockdep_nr_slabs)
+		return 0;
+
+	/* Compute headroom requirement (default 100% or custom percentage) with 32-slab floor */
+	kept_headroom_slabs = DIV_ROUND_UP(used * requested_lockdep_headroom_pct, 100);
+	if (kept_headroom_slabs < 32)
+		kept_headroom_slabs = 32;
+
+	target_slabs = used + kept_headroom_slabs;
+
+	/* Satisfy both: target_slabs >= requested_lockdep_slabs AND headroom >= M% */
+	if (requested_lockdep_slabs > target_slabs)
+		target_slabs = requested_lockdep_slabs;
+
+	target_slabs = clamp_t(unsigned int, target_slabs, used, lockdep_nr_slabs);
+
+	/* Release excess slabs to buddy allocator */
+	if (target_slabs < lockdep_nr_slabs) {
+		for (i = target_slabs; i < lockdep_nr_slabs; i++) {
+			struct page *page = virt_to_page(lockdep_slabs[i]);
+			unsigned long p;
+
+			for (p = 0; p < (LOCKDEP_SLAB_SIZE >> PAGE_SHIFT); p++)
+				free_reserved_page(page + p);
+
+			lockdep_slabs[i] = NULL;
+		}
+		freed_slabs = lockdep_nr_slabs - target_slabs;
+		lockdep_nr_slabs = target_slabs;
+	}
+
+	pr_info("lockdep: boot complete : %u/%u slabs used, %u kept (%u%% headroom), %u returned to buddy (%u kB freed)\n",
+		used, initial_slabs, lockdep_nr_slabs,
+		lockdep_nr_slabs > used ? ((lockdep_nr_slabs - used) * 100) / used : 0,
+		freed_slabs, (freed_slabs * LOCKDEP_SLAB_SIZE) / 1024);
+
+	register_reboot_notifier(&lockdep_reboot_nb);
+	return 0;
+}
+late_initcall_sync(lockdep_post_init_trim);
+
 static void
 print_freed_lock_bug(struct task_struct *curr, const void *mem_from,
 		     const void *mem_to, struct held_lock *hlock)
diff --git a/kernel/locking/lockdep_proc.c b/kernel/locking/lockdep_proc.c
index 1916db9aa46b..95b76047918d 100644
--- a/kernel/locking/lockdep_proc.c
+++ b/kernel/locking/lockdep_proc.c
@@ -32,16 +32,17 @@
  * bitmap and max_lock_class_idx.
  */
 #define iterate_lock_classes(idx, class)				\
-	for (idx = 0, class = lock_classes; idx <= max_lock_class_idx;	\
-	     idx++, class++)
+	for (idx = 0, class = idx_to_lock_class(0);			\
+	     idx <= max_lock_class_idx;					\
+	     idx++, class = idx_to_lock_class(idx))
 
 static void *l_next(struct seq_file *m, void *v, loff_t *pos)
 {
-	struct lock_class *class = v;
+	unsigned long idx = ++*pos;
 
-	++class;
-	*pos = class - lock_classes;
-	return (*pos > max_lock_class_idx) ? NULL : class;
+	if (idx > max_lock_class_idx)
+		return NULL;
+	return idx_to_lock_class(idx);
 }
 
 static void *l_start(struct seq_file *m, loff_t *pos)
@@ -50,7 +51,7 @@ static void *l_start(struct seq_file *m, loff_t *pos)
 
 	if (idx > max_lock_class_idx)
 		return NULL;
-	return lock_classes + idx;
+	return idx_to_lock_class(idx);
 }
 
 static void l_stop(struct seq_file *m, void *v)
@@ -59,7 +60,7 @@ static void l_stop(struct seq_file *m, void *v)
 
 static void print_name(struct seq_file *m, struct lock_class *class)
 {
-	char str[KSYM_NAME_LEN];
+	char str[128];
 	const char *name = class->name;
 
 	if (!name) {
@@ -79,9 +80,9 @@ static int l_show(struct seq_file *m, void *v)
 	struct lock_class *class = v;
 	struct lock_list *entry;
 	char usage[LOCK_USAGE_CHARS];
-	int idx = class - lock_classes;
+	int idx = class->class_idx;
 
-	if (v == lock_classes)
+	if (idx == 0)
 		seq_printf(m, "all lock classes:\n");
 
 	if (!test_bit(idx, lock_classes_in_use))
@@ -133,7 +134,7 @@ static void *lc_start(struct seq_file *m, loff_t *pos)
 	if (*pos == 0)
 		return SEQ_START_TOKEN;
 
-	return lock_chains + (*pos - 1);
+	return idx_to_lock_chain(*pos - 1);
 }
 
 static void *lc_next(struct seq_file *m, void *v, loff_t *pos)

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 7/8] lockdep: Expose slab pool telemetry in /proc/lockdep_stats and initcalls
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (5 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 6/8] lockdep: Free unused reservation slabs to buddy allocator at late boot Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  3:58 ` [PATCH 8/8] lockdep: on debug_locks_off or OOM, recycle all slabs to buddy Jim Cromie
  2026-08-27  6:46 ` [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Peter Zijlstra
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

Extend lockdep observability to track slab allocation dynamics:

0. In /proc/lockdep_stats, display total reserved slabs, used slabs,
   free slabs (headroom), and a per-consumer slab breakdown (lock_classes,
   direct deps, dependency chains, chain hlocks, stack traces).

1. In lockdep.c, add lockdep_report_stage() hooks registered across
   initcall milestones (core, postcore, arch, subsys, fs, device, late)
   to log slab consumption progress throughout kernel initialization.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c           | 47 ++++++++++++++++++++++++++++++++++++++
 kernel/locking/lockdep_internals.h |  4 +++-
 kernel/locking/lockdep_proc.c      | 40 ++++++++++++++++++++++++--------
 3 files changed, 80 insertions(+), 11 deletions(-)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index 3fb6c07cea36..b6048e7c8b56 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -6992,6 +6992,53 @@ void __init lockdep_init(void)
 	       sizeof(((struct task_struct *)NULL)->held_locks));
 }
 
+static void lockdep_report_stage(const char *domain, const char *stage_name)
+{
+	unsigned int used = lockdep_slabs_used - lockdep_nr_free_slabs;
+	unsigned int class_per_chunk = lock_class_PER_CHUNK;
+	unsigned int entry_per_chunk = LOCKDEP_SLAB_SIZE / sizeof(struct lock_list);
+	unsigned int chain_per_chunk = lock_chain_PER_CHUNK;
+	unsigned int hlock_per_chunk = chain_hlock_PER_CHUNK;
+	unsigned int trace_per_chunk = LOCKDEP_SLAB_SIZE / sizeof(unsigned long);
+
+	unsigned int cl_w = nr_lock_classes / class_per_chunk;
+	unsigned int cl_f = ((nr_lock_classes % class_per_chunk) * 100) / class_per_chunk;
+
+	unsigned int en_w = nr_list_entries / entry_per_chunk;
+	unsigned int en_f = ((nr_list_entries % entry_per_chunk) * 100) / entry_per_chunk;
+
+	unsigned long chains = lock_chain_count();
+	unsigned int ch_w = chains / chain_per_chunk;
+	unsigned int ch_f = ((chains % chain_per_chunk) * 100) / chain_per_chunk;
+
+	unsigned int hlocks = chain_hlocks_used();
+	unsigned int hl_w = hlocks / hlock_per_chunk;
+	unsigned int hl_f = ((hlocks % hlock_per_chunk) * 100) / hlock_per_chunk;
+
+	unsigned int tr_w = nr_stack_trace_entries / trace_per_chunk;
+	unsigned int tr_f = ((nr_stack_trace_entries % trace_per_chunk) * 100) / trace_per_chunk;
+
+	pr_info("lockdep: %-8s [%-9s] : %u/%u slabs : classes=%u.%02u entries=%u.%02u chains=%u.%02u hlocks=%u.%02u trace=%u.%02u\n",
+		domain, stage_name, used, lockdep_nr_slabs,
+		cl_w, cl_f, en_w, en_f, ch_w, ch_f, hl_w, hl_f, tr_w, tr_f);
+}
+
+#define DEFINE_LOCKDEP_LEVEL_REPORT(lvl, name)			\
+	static int __init lockdep_report_##name(void)		\
+	{							\
+		lockdep_report_stage("initcall", #name);	\
+		return 0;					\
+	}							\
+	lvl(lockdep_report_##name)
+
+DEFINE_LOCKDEP_LEVEL_REPORT(core_initcall_sync, core);
+DEFINE_LOCKDEP_LEVEL_REPORT(postcore_initcall_sync, postcore);
+DEFINE_LOCKDEP_LEVEL_REPORT(arch_initcall_sync, arch);
+DEFINE_LOCKDEP_LEVEL_REPORT(subsys_initcall_sync, subsys);
+DEFINE_LOCKDEP_LEVEL_REPORT(fs_initcall_sync, fs);
+DEFINE_LOCKDEP_LEVEL_REPORT(device_initcall_sync, device);
+DEFINE_LOCKDEP_LEVEL_REPORT(late_initcall_sync, late);
+
 static int lockdep_shutdown_notify(struct notifier_block *nb,
 				   unsigned long code, void *unused)
 {
diff --git a/kernel/locking/lockdep_internals.h b/kernel/locking/lockdep_internals.h
index ccd7343af672..6fb776618ac6 100644
--- a/kernel/locking/lockdep_internals.h
+++ b/kernel/locking/lockdep_internals.h
@@ -223,7 +223,7 @@ extern unsigned int max_lockdep_depth;
 extern unsigned int max_bfs_queue_depth;
 extern unsigned long max_lock_class_idx;
 
-DECLARE_2D_RADIX(lock_class, struct lock_class);
+DECLARE_CHUNKED_ARRAY(lock_class, struct lock_class);
 extern unsigned long lock_classes_in_use[];
 
 struct lockdep_slab_usage {
@@ -240,6 +240,8 @@ struct lockdep_slab_stats {
 	struct lockdep_slab_usage usage;
 };
 
+void lockdep_get_slab_stats(struct lockdep_slab_stats *st);
+
 unsigned int chain_hlocks_used(void);
 unsigned long lock_chain_count(void);
 void lockdep_get_slab_stats(struct lockdep_slab_stats *st);
diff --git a/kernel/locking/lockdep_proc.c b/kernel/locking/lockdep_proc.c
index 95b76047918d..76e32fca8c54 100644
--- a/kernel/locking/lockdep_proc.c
+++ b/kernel/locking/lockdep_proc.c
@@ -380,17 +380,37 @@ static int lockdep_stats_show(struct seq_file *m, void *v)
 			debug_locks);
 
 	/*
-	 * Zapped classes and lockdep data buffers reuse statistics.
+	 * Shared Memblock Slab Reservoir Statistics
 	 */
-	seq_puts(m, "\n");
-	seq_printf(m, " zapped classes:                %11lu\n",
-			nr_zapped_classes);
-#ifdef CONFIG_PROVE_LOCKING
-	seq_printf(m, " zapped lock chains:            %11lu\n",
-			nr_zapped_lock_chains);
-	seq_printf(m, " large chain blocks:            %11u\n",
-			nr_large_chain_blocks);
-#endif
+	{
+		struct lockdep_slab_stats st;
+
+		lockdep_get_slab_stats(&st);
+		if (st.total_slabs) {
+			unsigned int free_slabs = st.total_slabs > st.used_slabs ?
+						  st.total_slabs - st.used_slabs : 0;
+			unsigned int headroom_pct = (free_slabs * 100) / st.total_slabs;
+
+			seq_puts(m, "\n lockdep memblock slab reservoir:\n");
+			seq_printf(m, "   total slabs:                 %11u (%zu kB)\n",
+				   st.total_slabs, (size_t)st.total_slabs * 64);
+			seq_printf(m, "   used slabs:                  %11u (%zu kB)\n",
+				   st.used_slabs, (size_t)st.used_slabs * 64);
+			seq_printf(m, "     - lock_classes slabs:      %11u (%zu kB)\n",
+				   st.usage.lock_classes, (size_t)st.usage.lock_classes * 64);
+			seq_printf(m, "     - direct deps slabs:       %11u (%zu kB)\n",
+				   st.usage.direct_deps, (size_t)st.usage.direct_deps * 64);
+			seq_printf(m, "     - dependency chains slabs: %11u (%zu kB)\n",
+				   st.usage.lock_chains, (size_t)st.usage.lock_chains * 64);
+			seq_printf(m, "     - chain hlocks slabs:      %11u (%zu kB)\n",
+				   st.usage.chain_hlocks, (size_t)st.usage.chain_hlocks * 64);
+			seq_printf(m, "     - stack_trace slabs:       %11u (%zu kB)\n",
+				   st.usage.stack_traces, (size_t)st.usage.stack_traces * 64);
+			seq_printf(m, "   free slabs (headroom):       %11u (%zu kB, %u%%)\n",
+				   free_slabs, (size_t)free_slabs * 64, headroom_pct);
+		}
+	}
+
 	return 0;
 }
 

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* [PATCH 8/8] lockdep: on debug_locks_off or OOM, recycle all slabs to buddy
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (6 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 7/8] lockdep: Expose slab pool telemetry in /proc/lockdep_stats and initcalls Jim Cromie
@ 2026-08-27  3:58 ` Jim Cromie
  2026-08-27  6:46 ` [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Peter Zijlstra
  8 siblings, 0 replies; 13+ messages in thread
From: Jim Cromie @ 2026-08-27  3:58 UTC (permalink / raw)
  To: Peter Zijlstra, Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long
  Cc: linux-kernel, Jim Cromie

If lockdep breaks, by assertion failure or for ENOMEM-ish reasons, we
can no longer use the graph-db.  Since most of the graph-db is now
allocated from memblock_alloc() slabs, we can release them all back to
buddy, and hope that freeing ~1.5MB will help. (v7.2 has ~10MB tied up
in .bss).

0. Hook debug_locks_off() / print_lockdep_off() via an asynchronous
   work item (lockdep_sacrifice_work) to release all held slabs back
   to the buddy allocator via free_reserved_page() in process context.

1. Register an OOM notifier (lockdep_oom_nb) at late_initcall. If the
   system encounters an out-of-memory emergency, lockdep sacrifices its
   entire dynamic slab pool (1..8 MB of physical RAM), reporting freed
   pages to the OOM killer to avoid terminating user processes.

2. Clear lockdep_slabs[] pointers and zero lockdep_nr_slabs under
   graph_lock to seal off subsequent allocations.

Signed-off-by: Jim Cromie <jim.cromie@gmail.com>
---
 kernel/locking/lockdep.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 81 insertions(+)

diff --git a/kernel/locking/lockdep.c b/kernel/locking/lockdep.c
index b6048e7c8b56..9f28530b2f1d 100644
--- a/kernel/locking/lockdep.c
+++ b/kernel/locking/lockdep.c
@@ -60,7 +60,9 @@
 #include <linux/kasan.h>
 #include <linux/mm.h>
 #include <linux/memblock.h>
+#include <linux/oom.h>
 #include <linux/reboot.h>
+#include <linux/workqueue.h>
 
 #include <asm/sections.h>
 
@@ -638,6 +640,82 @@ static int verbose(struct lock_class *class)
 	return 0;
 }
 
+/*
+ * Release all memblock slabs (used and unused) back to the buddy allocator
+ * when lockdep is disabled or during system OOM emergencies.
+ * Must run in process context (workqueue or OOM notifier).
+ */
+static unsigned int lockdep_release_slabs_to_buddy(void)
+{
+	unsigned int freed_slabs = 0;
+	unsigned int nr = lockdep_nr_slabs;
+	unsigned long flags;
+	unsigned int i;
+
+	if (!nr)
+		return 0;
+
+	/* Invalidate table bounds under graph_lock */
+	raw_local_irq_save(flags);
+	if (!graph_lock()) {
+		raw_local_irq_restore(flags);
+		return 0;
+	}
+	lockdep_nr_slabs = 0;
+	lockdep_slabs_used = 0;
+	graph_unlock();
+	raw_local_irq_restore(flags);
+
+	for (i = 0; i < nr; i++) {
+		struct page *page;
+		unsigned long p;
+
+		if (!lockdep_slabs[i])
+			continue;
+
+		page = virt_to_page(lockdep_slabs[i]);
+		for (p = 0; p < (LOCKDEP_SLAB_SIZE >> PAGE_SHIFT); p++)
+			free_reserved_page(page + p);
+
+		lockdep_slabs[i] = NULL;
+		freed_slabs++;
+	}
+
+	if (freed_slabs)
+		pr_info("lockdep: emergency sacrifice — released %u slabs (%u kB) to buddy allocator\n",
+			freed_slabs, (freed_slabs * LOCKDEP_SLAB_SIZE) / 1024);
+
+	return freed_slabs;
+}
+
+static void lockdep_sacrifice_work_fn(struct work_struct *work)
+{
+	lockdep_release_slabs_to_buddy();
+}
+static DECLARE_WORK(lockdep_sacrifice_work, lockdep_sacrifice_work_fn);
+
+static int lockdep_oom_notify(struct notifier_block *self,
+			      unsigned long dummy, void *parm)
+{
+	unsigned long *freed = parm;
+	unsigned int freed_slabs;
+
+	if (!lockdep_nr_slabs)
+		return NOTIFY_OK;
+
+	/* Turn off lockdep before sacrificing tables */
+	debug_locks_off();
+	freed_slabs = lockdep_release_slabs_to_buddy();
+	if (freed && freed_slabs)
+		*freed += (freed_slabs * (LOCKDEP_SLAB_SIZE >> PAGE_SHIFT));
+
+	return NOTIFY_OK;
+}
+
+static struct notifier_block lockdep_oom_nb = {
+	.notifier_call = lockdep_oom_notify,
+};
+
 static void print_lockdep_off(const char *bug_msg)
 {
 	printk(KERN_DEBUG "%s\n", bug_msg);
@@ -645,6 +723,8 @@ static void print_lockdep_off(const char *bug_msg)
 #ifdef CONFIG_LOCK_STAT
 	printk(KERN_DEBUG "Please attach the output of /proc/lock_stat to the bug report\n");
 #endif
+	if (system_state >= SYSTEM_RUNNING)
+		schedule_work(&lockdep_sacrifice_work);
 }
 
 unsigned long nr_stack_trace_entries;
@@ -7105,6 +7185,7 @@ static int __init lockdep_post_init_trim(void)
 		lockdep_nr_slabs > used ? ((lockdep_nr_slabs - used) * 100) / used : 0,
 		freed_slabs, (freed_slabs * LOCKDEP_SLAB_SIZE) / 1024);
 
+	register_oom_notifier(&lockdep_oom_nb);
 	register_reboot_notifier(&lockdep_reboot_nb);
 	return 0;
 }

-- 
2.55.0


^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool
  2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
                   ` (7 preceding siblings ...)
  2026-08-27  3:58 ` [PATCH 8/8] lockdep: on debug_locks_off or OOM, recycle all slabs to buddy Jim Cromie
@ 2026-08-27  6:46 ` Peter Zijlstra
  2026-08-27  8:54   ` jim.cromie
  8 siblings, 1 reply; 13+ messages in thread
From: Peter Zijlstra @ 2026-08-27  6:46 UTC (permalink / raw)
  To: Jim Cromie
  Cc: Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long, linux-kernel

On Wed, Aug 26, 2026 at 09:58:32PM -0600, Jim Cromie wrote:
> Lockdep cannot rely upon any other subsystem that uses locks, so since
> inception, its graph-db has been stored in static arrays, pinning ~10
> MB in .bss. This is a hardcoded compromise between embedded and
> enterprise hardware.
> 
> However, if it acts early, lockdep can pre-allocate a pool of slabs
> from memblock_alloc(), enough for its lifetime of anticipated workloads.
> Then it can allocate them as needed to provide new segments/slabs to
> the graph-db.
> 

Why? I really don't understand why. Who cares about this bss stuff.

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool
  2026-08-27  6:46 ` [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Peter Zijlstra
@ 2026-08-27  8:54   ` jim.cromie
  2026-08-27  9:03     ` Peter Zijlstra
  0 siblings, 1 reply; 13+ messages in thread
From: jim.cromie @ 2026-08-27  8:54 UTC (permalink / raw)
  To: Peter Zijlstra
  Cc: Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long, linux-kernel

On Thu, Aug 27, 2026 at 12:46 AM Peter Zijlstra <peterz@infradead.org> wrote:
>
> On Wed, Aug 26, 2026 at 09:58:32PM -0600, Jim Cromie wrote:
> > Lockdep cannot rely upon any other subsystem that uses locks, so since
> > inception, its graph-db has been stored in static arrays, pinning ~10
> > MB in .bss. This is a hardcoded compromise between embedded and
> > enterprise hardware.
> >
> > However, if it acts early, lockdep can pre-allocate a pool of slabs
> > from memblock_alloc(), enough for its lifetime of anticipated workloads.
> > Then it can allocate them as needed to provide new segments/slabs to
> > the graph-db.
> >
>
> Why? I really don't understand why. Who cares about this bss stuff.

I thought embedded folk might value 10mb less bss ?
Or have they stopped using lockdep already, for size or other reasons.

How about OOM, when kernel needs mem,
or lockdep debug-off, when the slabs tied up in the graph-db could be returned

do these not tip the scales ?

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool
  2026-08-27  8:54   ` jim.cromie
@ 2026-08-27  9:03     ` Peter Zijlstra
  2026-08-27 18:40       ` jim.cromie
  0 siblings, 1 reply; 13+ messages in thread
From: Peter Zijlstra @ 2026-08-27  9:03 UTC (permalink / raw)
  To: jim.cromie
  Cc: Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long, linux-kernel

On Thu, Aug 27, 2026 at 02:54:12AM -0600, jim.cromie@gmail.com wrote:
> On Thu, Aug 27, 2026 at 12:46 AM Peter Zijlstra <peterz@infradead.org> wrote:
> >
> > On Wed, Aug 26, 2026 at 09:58:32PM -0600, Jim Cromie wrote:
> > > Lockdep cannot rely upon any other subsystem that uses locks, so since
> > > inception, its graph-db has been stored in static arrays, pinning ~10
> > > MB in .bss. This is a hardcoded compromise between embedded and
> > > enterprise hardware.
> > >
> > > However, if it acts early, lockdep can pre-allocate a pool of slabs
> > > from memblock_alloc(), enough for its lifetime of anticipated workloads.
> > > Then it can allocate them as needed to provide new segments/slabs to
> > > the graph-db.
> > >
> >
> > Why? I really don't understand why. Who cares about this bss stuff.
> 
> I thought embedded folk might value 10mb less bss ?
> Or have they stopped using lockdep already, for size or other reasons.

I've never heard complaints from embedded people that this is a problem.
Very few Linux capable machines can't spare 10mb.

This is about kernel development, if you need to develop a driver (only
case you might be tied to specific hardware) just get your developer a
board that has a spare 10mb of memory? Your developer is probably
served by having the most beefy board available anyway.

There was a case on sparc where the bss was a problem because the kernel
image had definite size constraints, but I don't think any 'modern'
systems suffer that particular problem.

> How about OOM, when kernel needs mem,
> or lockdep debug-off, when the slabs tied up in the graph-db could be returned

If you're running into OOM while doing kernel dev you're doing it wrong?

^ permalink raw reply	[flat|nested] 13+ messages in thread

* Re: [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool
  2026-08-27  9:03     ` Peter Zijlstra
@ 2026-08-27 18:40       ` jim.cromie
  0 siblings, 0 replies; 13+ messages in thread
From: jim.cromie @ 2026-08-27 18:40 UTC (permalink / raw)
  To: Peter Zijlstra
  Cc: Ingo Molnar, Will Deacon, Boqun Feng, Waiman Long, linux-kernel

On Thu, Aug 27, 2026 at 3:03 AM Peter Zijlstra <peterz@infradead.org> wrote:
>
> On Thu, Aug 27, 2026 at 02:54:12AM -0600, jim.cromie@gmail.com wrote:
> > On Thu, Aug 27, 2026 at 12:46 AM Peter Zijlstra <peterz@infradead.org> wrote:
> > >
> > > On Wed, Aug 26, 2026 at 09:58:32PM -0600, Jim Cromie wrote:
> > > > Lockdep cannot rely upon any other subsystem that uses locks, so since
> > > > inception, its graph-db has been stored in static arrays, pinning ~10
> > > > MB in .bss. This is a hardcoded compromise between embedded and
> > > > enterprise hardware.
> > > >
> > > > However, if it acts early, lockdep can pre-allocate a pool of slabs
> > > > from memblock_alloc(), enough for its lifetime of anticipated workloads.
> > > > Then it can allocate them as needed to provide new segments/slabs to
> > > > the graph-db.
> > > >
> > >
> > > Why? I really don't understand why. Who cares about this bss stuff.
> >
> > I thought embedded folk might value 10mb less bss ?
> > Or have they stopped using lockdep already, for size or other reasons.
>
> I've never heard complaints from embedded people that this is a problem.
> Very few Linux capable machines can't spare 10mb.
>
> This is about kernel development, if you need to develop a driver (only
> case you might be tied to specific hardware) just get your developer a
> board that has a spare 10mb of memory? Your developer is probably
> served by having the most beefy board available anyway.
>
> There was a case on sparc where the bss was a problem because the kernel
> image had definite size constraints, but I don't think any 'modern'
> systems suffer that particular problem.
>

Fair points on embedded.
So the value proposition is narrow:

folks hitting "BUG: MAX_LOCKDEP_* too low!"
who cannot build, deploy a kernel with tweaked MAX_LOCKDEP constants.
They're running a distro-debug kernel.

this group might include:
Distro QA, enterprise testers, Syzbot/CI runners.
For these users, lockdep sometimes turns off permanently,
silently invalidating the rest of the test run.

if they had the lockdep_slabs=N knob, they might use it,
and throw more workload on the box without a possible hard-fail looming.

> > How about OOM, when kernel needs mem,
> > or lockdep debug-off, when the slabs tied up in the graph-db could be returned
>
> If you're running into OOM while doing kernel dev you're doing it wrong?

heh - not me, that was the other guy.
it was a "feature", I thought it might help the sale. :-)

 thanks

^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2026-08-27 18:41 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-27  3:58 [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Jim Cromie
2026-08-27  3:58 ` [PATCH 1/8] lockdep: Traverse adjacency lists directly in zap_class() Jim Cromie
2026-08-27  3:58 ` [PATCH 2/8] lockdep: Add chunked array infrastructure and embedded indices Jim Cromie
2026-08-27  3:58 ` [PATCH 3/8] lockdep: Pre-reserve early memblock slab pool for dynamic tables Jim Cromie
2026-08-27  3:58 ` [PATCH 4/8] lockdep: Convert 5 graph arrays to chunked tables backed by slab pool Jim Cromie
2026-08-27  3:58 ` [PATCH 5/8] lockdep: Fast-path power-of-2 tables with shift/mask indexing Jim Cromie
2026-08-27  3:58 ` [PATCH 6/8] lockdep: Free unused reservation slabs to buddy allocator at late boot Jim Cromie
2026-08-27  3:58 ` [PATCH 7/8] lockdep: Expose slab pool telemetry in /proc/lockdep_stats and initcalls Jim Cromie
2026-08-27  3:58 ` [PATCH 8/8] lockdep: on debug_locks_off or OOM, recycle all slabs to buddy Jim Cromie
2026-08-27  6:46 ` [PATCH 0/8] lockdep: change 5 graph-db arrays to AofAs, fill from memblock pool Peter Zijlstra
2026-08-27  8:54   ` jim.cromie
2026-08-27  9:03     ` Peter Zijlstra
2026-08-27 18:40       ` jim.cromie

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®