Linux Btrfs filesystem development
 help / color / mirror / Atom feed
* [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters
@ 2026-04-19 14:26 Torstein Eide
  2026-04-19 14:26 ` [PATCH 1/5] btrfs: uapi: introduce on-disk scrub stats item Torstein Eide
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

btrfs currently exposes scrub progress only via ioctl which returns a
snapshot of a running scrub and nothing once it finishes.  There is no
persistent record of what previous scrubs found and no sysfs interface
for monitoring.

This series adds per-device scrub statistics that survive across
unmounts and are exposed via sysfs.  A new on-disk item
(BTRFS_SCRUB_STATS_OBJECTID, BTRFS_PERSISTENT_ITEM_KEY, devid) stores
14 __le64 lifetime counters per device in the device tree, following
the same dirty-counter/flush-at-commit pattern as btrfs_dev_stats.

Each device gains an in-memory session snapshot (reset at scrub
start, populated at scrub end) for the most recent run.

The sysfs layout under /sys/fs/btrfs/<UUID>/ and devinfo/<devid>/:

  scrub/lifetime/<counter>    accumulated totals across all runs
  scrub/session/<counter>     snapshot of the most recent run
  scrub/session/status        idle | running | finished | canceled
  scrub/session/duration_seconds
  scrub/reset                 write "1" to zero lifetime counters

plan is explaned her: https://github.com/kdave/btrfs-progs/issues/1108

tested with xfstests.

## Result

Bellow is table with results of a destrutive test, with two passes,  one  `dd` per devices, scrub repeat. 

| FILE                  | Total                   | Device 1                | Device  2               |
|                       | Lifetime   | Session    | Lifetime   | Session    | Lifetime   | Session    |
|-----------------------|------------|------------|------------|------------|------------|------------|
| corrected_errors      | 28736      | 13120      | 15616      | 0          | 13120      | 13120      |
| csum_discards         | 0          | 0          | 0          | 0          | 0          | 0          |
| csum_errors           | 28736      | 13120      | 15616      | 0          | 13120      | 13120      |
| data_bytes_scrubbed   | 6174015488 | 3087007744 | 3087007744 | 1543503872 | 3087007744 | 1543503872 |
| data_extents_scrubbed | 94208      | 47104      | 47104      | 23552      | 47104      | 23552      |
| duration_seconds      |            | 1          |            | 1          |            | 1          |
| last_physical         |            |            |            | 2147483648 |            | 2126643200 |
| malloc_errors         | 0          | 0          | 0          | 0          | 0          | 0          |
| no_csum               | 0          | 0          | 0          | 0          | 0          | 0          |
| read_errors           | 0          | 0          | 0          | 0          | 0          | 0          |
| status                |            | finished   |            | finished   |            | finished   |
| super_errors          | 0          | 0          | 0          | 0          | 0          | 0          |
| t_end                 |            | 1776595754 |            | 1776595754 |            | 1776595754 |
| tree_bytes_scrubbed   | 6946816    | 3473408    | 3473408    | 1736704    | 3473408    | 1736704    |
| tree_extents_scrubbed | 424        | 212        | 212        | 106        | 212        | 106        |
| t_start               |            | 1776595753 |            | 1776595753 |            | 1776595753 |
| uncorrectable_errors  | 0          | 0          | 0          | 0          | 0          | 0          |
| unverified_errors     | 0          | 0          | 0          | 0          | 0          | 0          |
| verify_errors         | 0          | 0          | 0          | 0          | 0          | 0          |


Torstein Eide (5):
  btrfs: uapi: introduce on-disk scrub stats item
  btrfs: add in-memory scrub lifetime and session fields
  btrfs: persist scrub lifetime stats to the device tree
  btrfs: hook scrub session tracking into 'btrfs_scrub_dev()'
  btrfs: expose scrub lifetime and session counters via sysfs

 fs/btrfs/disk-io.c              |   6 +
 fs/btrfs/fs.h                   |   5 +
 fs/btrfs/scrub.c                |  16 +
 fs/btrfs/sysfs.c                | 586 ++++++++++++++++++++++++++++++++
 fs/btrfs/sysfs.h                |   3 +
 fs/btrfs/transaction.c          |   3 +
 fs/btrfs/volumes.c              | 239 ++++++++++++-
 fs/btrfs/volumes.h              |  79 +++++
 include/uapi/linux/btrfs_tree.h |  34 ++
 9 files changed, 969 insertions(+), 2 deletions(-)

-- 
2.48.1


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

* [PATCH 1/5] btrfs: uapi: introduce on-disk scrub stats item
  2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
@ 2026-04-19 14:26 ` Torstein Eide
  2026-04-19 14:26 ` [PATCH 2/5] btrfs: add in-memory scrub lifetime and session fields Torstein Eide
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

Add a new persistent item type for per-device scrub lifetime counters:

  Key: (BTRFS_SCRUB_STATS_OBJECTID, BTRFS_PERSISTENT_ITEM_KEY, devid)

The item payload is struct btrfs_scrub_stats_item, an array of __le64
values indexed by BTRFS_SCRUB_STAT_* constants (data/tree extents and
bytes scrubbed, read/checksum/verify/malloc errors, etc.).

The array is designed to grow at the end; existing index values are
fixed and must not be renumbered.  New entries should be appended and
the item_size check in the reader handles shorter on-disk items from
older kernels gracefully.

Signed-off-by: Torstein Eide <torsteine@gmail.com>
Assisted-by: Claude:claude-sonnet-4-6
---
 include/uapi/linux/btrfs_tree.h | 34 +++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h
index cc3b9f7dccafa..7eb775b6b6253 100644
--- a/include/uapi/linux/btrfs_tree.h
+++ b/include/uapi/linux/btrfs_tree.h
@@ -82,6 +82,9 @@
 /* device stats in the device tree */
 #define BTRFS_DEV_STATS_OBJECTID 0ULL
 
+/* per-device scrub lifetime stats in the device tree */
+#define BTRFS_SCRUB_STATS_OBJECTID 1ULL
+
 /* for storing balance parameters in the root tree */
 #define BTRFS_BALANCE_OBJECTID -4ULL
 
@@ -1139,6 +1142,37 @@ struct btrfs_dev_stats_item {
 	__le64 values[BTRFS_DEV_STAT_VALUES_MAX];
 } __attribute__ ((__packed__));
 
+/*
+ * Scrub lifetime error counters, stored per device in the device tree.
+ * Key: (BTRFS_SCRUB_STATS_OBJECTID, BTRFS_PERSISTENT_ITEM_KEY, devid)
+ *
+ * Index values are defined by enum btrfs_scrub_stat_index in btrfs_tree.h
+ * (kernel-internal, not exposed via ioctl).
+ */
+#define BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED		0
+#define BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED		1
+#define BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED		2
+#define BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED		3
+#define BTRFS_SCRUB_STAT_READ_ERRORS			4
+#define BTRFS_SCRUB_STAT_CSUM_ERRORS			5
+#define BTRFS_SCRUB_STAT_VERIFY_ERRORS			6
+#define BTRFS_SCRUB_STAT_NO_CSUM			7
+#define BTRFS_SCRUB_STAT_CSUM_DISCARDS			8
+#define BTRFS_SCRUB_STAT_SUPER_ERRORS			9
+#define BTRFS_SCRUB_STAT_MALLOC_ERRORS			10
+#define BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS		11
+#define BTRFS_SCRUB_STAT_CORRECTED_ERRORS		12
+#define BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS		13
+#define BTRFS_SCRUB_STAT_VALUES_MAX			14
+
+struct btrfs_scrub_stats_item {
+	/*
+	 * Grow at the end for future enhancements; keep existing values
+	 * unchanged.  Index values defined by BTRFS_SCRUB_STAT_* above.
+	 */
+	__le64 values[BTRFS_SCRUB_STAT_VALUES_MAX];
+} __attribute__ ((__packed__));
+
 #define BTRFS_DEV_REPLACE_ITEM_CONT_READING_FROM_SRCDEV_MODE_ALWAYS	0
 #define BTRFS_DEV_REPLACE_ITEM_CONT_READING_FROM_SRCDEV_MODE_AVOID	1
 
-- 
2.48.1


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

* [PATCH 2/5] btrfs: add in-memory scrub lifetime and session fields
  2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
  2026-04-19 14:26 ` [PATCH 1/5] btrfs: uapi: introduce on-disk scrub stats item Torstein Eide
@ 2026-04-19 14:26 ` Torstein Eide
  2026-04-19 14:26 ` [PATCH 3/5] btrfs: persist scrub lifetime stats to the device tree Torstein Eide
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

Extend struct btrfs_device with two counter arrays and session metadata:

  scrub_stat_values[]    - lifetime totals, persisted across mounts via
                           the new BTRFS_SCRUB_STATS_OBJECTID tree item.
                           Protected by an atomic dirty counter
                           (scrub_stats_ccnt) and flushed at transaction
                           commit by btrfs_run_scrub_stats().

  scrub_session_values[] - per-run counters reset when a new scrub
                           starts; never written to disk.

  scrub_session_{t_start,t_end,last_physical,status} - timing and
                           progress metadata for the current or most
                           recent scrub session on this device.

Add matching kobject pointers (scrub_kobj, scrub_lifetime_kobj,
scrub_session_kobj) to both struct btrfs_device and struct btrfs_fs_info
for the sysfs hierarchy added in a later patch.

Add BTRFS_SCRUB_STATUS_* constants and the inline read/write helpers
btrfs_scrub_stat_{read,add,set}() and btrfs_scrub_session_read().
The add/set helpers include an smp_mb__before_atomic() to order stat
updates before the dirty-counter increment, pairing with the smp_rmb()
in btrfs_run_scrub_stats().

Signed-off-by: Torstein Eide <torsteine@gmail.com>
Assisted-by: Claude:claude-sonnet-4-6
---
 fs/btrfs/fs.h      |  5 +++
 fs/btrfs/volumes.h | 79 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h
index a4758d94b32e9..c333e30c93e40 100644
--- a/fs/btrfs/fs.h
+++ b/fs/btrfs/fs.h
@@ -714,6 +714,11 @@ struct btrfs_fs_info {
 	struct kobject *qgroups_kobj;
 	struct kobject *discard_kobj;
 
+	/* For /sys/fs/btrfs/<UUID>/scrub/{lifetime,session}/ */
+	struct kobject *scrub_kobj;
+	struct kobject *scrub_lifetime_kobj;
+	struct kobject *scrub_session_kobj;
+
 	/* Track the number of blocks (sectors) read by the filesystem. */
 	struct percpu_counter stats_read_blocks;
 
diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h
index 0082c166af91f..e77f726928abd 100644
--- a/fs/btrfs/volumes.h
+++ b/fs/btrfs/volumes.h
@@ -200,6 +200,29 @@ struct btrfs_device {
 	atomic_t dev_stats_ccnt;
 	atomic_t dev_stat_values[BTRFS_DEV_STAT_VALUES_MAX];
 
+	/*
+	 * Scrub lifetime counters. Persisted via BTRFS_SCRUB_STATS_OBJECTID
+	 * tree items in the device tree; loaded at mount by
+	 * btrfs_init_scrub_stats(), flushed at commit by btrfs_run_scrub_stats().
+	 * Index values defined by BTRFS_SCRUB_STAT_* in btrfs_tree.h.
+	 */
+	int scrub_stats_valid;
+	atomic_t scrub_stats_ccnt;
+	atomic64_t scrub_stat_values[BTRFS_SCRUB_STAT_VALUES_MAX];
+
+	/*
+	 * Per-session scrub counters. Reset to zero when a new scrub starts on
+	 * this device. Never persisted to disk.
+	 */
+	atomic64_t scrub_session_values[BTRFS_SCRUB_STAT_VALUES_MAX];
+	/* Resume offset at end of session */
+	u64 scrub_session_last_physical;
+	/* Unix time (seconds) when the session started / ended (0 if idle) */
+	u64 scrub_session_t_start;
+	u64 scrub_session_t_end;
+	/* BTRFS_SCRUB_STATUS_* */
+	atomic_t scrub_session_status;
+
 	/*
 	 * Device's major-minor number. Must be set even if the device is not
 	 * opened (bdev == NULL), unless the device is missing.
@@ -211,6 +234,10 @@ struct btrfs_device {
 	struct completion kobj_unregister;
 	/* For sysfs/FSID/devinfo/devid/ */
 	struct kobject devid_kobj;
+	/* For sysfs/FSID/devinfo/devid/scrub/ hierarchy */
+	struct kobject *scrub_kobj;
+	struct kobject *scrub_lifetime_kobj;
+	struct kobject *scrub_session_kobj;
 
 	/* Bandwidth limit for scrub, in bytes */
 	u64 scrub_speed_max;
@@ -864,6 +891,58 @@ static inline void btrfs_dev_stat_set(struct btrfs_device *dev,
 	atomic_inc(&dev->dev_stats_ccnt);
 }
 
+/*
+ * Scrub session status values stored in device->scrub_session_status.
+ */
+#define BTRFS_SCRUB_STATUS_IDLE		0
+#define BTRFS_SCRUB_STATUS_RUNNING	1
+#define BTRFS_SCRUB_STATUS_FINISHED	2
+#define BTRFS_SCRUB_STATUS_CANCELED	3
+
+static inline u64 btrfs_scrub_stat_read(const struct btrfs_device *dev,
+					int index)
+{
+	return (u64)atomic64_read(&dev->scrub_stat_values[index]);
+}
+
+static inline void btrfs_scrub_stat_add(struct btrfs_device *dev,
+					int index, u64 val)
+{
+	atomic64_add((long long)val, &dev->scrub_stat_values[index]);
+	/*
+	 * Order the stat update before the dirty-counter increment so that
+	 * btrfs_run_scrub_stats() observes a consistent snapshot.  Pairs with
+	 * smp_rmb() in btrfs_run_scrub_stats().
+	 */
+	smp_mb__before_atomic();
+	atomic_inc(&dev->scrub_stats_ccnt);
+}
+
+static inline void btrfs_scrub_stat_set(struct btrfs_device *dev,
+					int index, u64 val)
+{
+	atomic64_set(&dev->scrub_stat_values[index], (long long)val);
+	/*
+	 * Order the stat update before the dirty-counter increment so that
+	 * btrfs_run_scrub_stats() observes a consistent snapshot.  Pairs with
+	 * smp_rmb() in btrfs_run_scrub_stats().
+	 */
+	smp_mb__before_atomic();
+	atomic_inc(&dev->scrub_stats_ccnt);
+}
+
+static inline u64 btrfs_scrub_session_read(const struct btrfs_device *dev,
+					   int index)
+{
+	return (u64)atomic64_read(&dev->scrub_session_values[index]);
+}
+
+int btrfs_init_scrub_stats(struct btrfs_fs_info *fs_info);
+int btrfs_run_scrub_stats(struct btrfs_trans_handle *trans);
+void btrfs_update_scrub_stats(struct btrfs_device *dev,
+			      const struct btrfs_scrub_progress *progress,
+			      int scrub_ret);
+
 static inline const char *btrfs_dev_name(const struct btrfs_device *device)
 {
 	if (!device || test_bit(BTRFS_DEV_STATE_MISSING, &device->dev_state))
-- 
2.48.1


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

* [PATCH 3/5] btrfs: persist scrub lifetime stats to the device tree
  2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
  2026-04-19 14:26 ` [PATCH 1/5] btrfs: uapi: introduce on-disk scrub stats item Torstein Eide
  2026-04-19 14:26 ` [PATCH 2/5] btrfs: add in-memory scrub lifetime and session fields Torstein Eide
@ 2026-04-19 14:26 ` Torstein Eide
  2026-04-19 14:26 ` [PATCH 4/5] btrfs: hook scrub session tracking into btrfs_scrub_dev() Torstein Eide
  2026-04-19 14:26 ` [PATCH 5/5] btrfs: expose scrub lifetime and session counters via sysfs Torstein Eide
  4 siblings, 0 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

Load and save per-device scrub lifetime counters using the on-disk item
introduced in the previous patch.

btrfs_init_scrub_stats() - called from open_ctree() after device stats
  are initialised.  Reads each device's BTRFS_SCRUB_STATS_OBJECTID item
  into the in-memory atomic arrays.  Devices with no item yet start with
  all counters at zero.  Items shorter than the current struct (from an
  older kernel) are handled by zero-filling the missing tail entries.

update_scrub_stat_item() / btrfs_run_scrub_stats() - mirrors the
  existing btrfs_run_dev_stats() pattern.  Called from
  commit_cowonly_roots() after btrfs_run_dev_stats().  Iterates over all
  devices; for each device whose scrub_stats_ccnt dirty counter is
  non-zero it writes the current atomic values back to the tree item,
  creating or replacing the item as needed.

btrfs_update_scrub_stats() - called from btrfs_scrub_dev() on
  completion (or cancellation).  Copies the final btrfs_scrub_progress
  counters into both the session arrays and the lifetime totals, records
  t_end and last_physical, and sets the session status.

Signed-off-by: Torstein Eide <torsteine@gmail.com>
Assisted-by: Claude:claude-sonnet-4-6
---
 fs/btrfs/disk-io.c     |   6 ++
 fs/btrfs/transaction.c |   3 +
 fs/btrfs/volumes.c     | 239 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 246 insertions(+), 2 deletions(-)

diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index 8a11be02eeb9b..fab08780e403e 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -3545,6 +3545,12 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device
 		goto fail_block_groups;
 	}
 
+	ret = btrfs_init_scrub_stats(fs_info);
+	if (ret) {
+		btrfs_err(fs_info, "failed to init scrub_stats: %d", ret);
+		goto fail_block_groups;
+	}
+
 	ret = btrfs_init_dev_replace(fs_info);
 	if (ret) {
 		btrfs_err(fs_info, "failed to init dev_replace: %d", ret);
diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c
index 248adb785051b..65689a3abbdbc 100644
--- a/fs/btrfs/transaction.c
+++ b/fs/btrfs/transaction.c
@@ -1375,6 +1375,9 @@ static noinline int commit_cowonly_roots(struct btrfs_trans_handle *trans)
 		return ret;
 
 	ret = btrfs_run_dev_stats(trans);
+	if (ret)
+		return ret;
+	ret = btrfs_run_scrub_stats(trans);
 	if (ret)
 		return ret;
 	ret = btrfs_run_dev_replace(trans);
diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c
index a88e68f905646..7de9396a52757 100644
--- a/fs/btrfs/volumes.c
+++ b/fs/btrfs/volumes.c
@@ -9,6 +9,7 @@
 #include <linux/ratelimit.h>
 #include <linux/kthread.h>
 #include <linux/semaphore.h>
+#include <linux/timekeeping.h>
 #include <linux/uuid.h>
 #include <linux/list_sort.h>
 #include <linux/namei.h>
@@ -8386,6 +8388,242 @@ int btrfs_get_dev_stats(struct btrfs_fs_info *fs_info,
 	return 0;
 }
 
+/* ---------- scrub lifetime stats: on-disk load/flush ---------- */
+
+static u64 btrfs_scrub_stats_value(const struct extent_buffer *eb,
+				   const struct btrfs_scrub_stats_item *ptr,
+				   int index)
+{
+	u64 val;
+
+	read_extent_buffer(eb, &val,
+			   offsetof(struct btrfs_scrub_stats_item, values) +
+			    ((unsigned long)ptr) + (index * sizeof(u64)),
+			   sizeof(val));
+	return le64_to_cpu(val);
+}
+
+static void btrfs_set_scrub_stats_value(struct extent_buffer *eb,
+					struct btrfs_scrub_stats_item *ptr,
+					int index, u64 val)
+{
+	__le64 leval = cpu_to_le64(val);
+
+	write_extent_buffer(eb, &leval,
+			    offsetof(struct btrfs_scrub_stats_item, values) +
+			     ((unsigned long)ptr) + (index * sizeof(u64)),
+			    sizeof(leval));
+}
+
+static int btrfs_device_init_scrub_stats(struct btrfs_device *device,
+					 struct btrfs_path *path)
+{
+	struct btrfs_scrub_stats_item *ptr;
+	struct extent_buffer *eb;
+	struct btrfs_key key;
+	int item_size;
+	int i, ret, slot;
+
+	key.objectid = BTRFS_SCRUB_STATS_OBJECTID;
+	key.type = BTRFS_PERSISTENT_ITEM_KEY;
+	key.offset = device->devid;
+
+	ret = btrfs_search_slot(NULL, device->fs_info->dev_root, &key, path, 0, 0);
+	if (ret) {
+		for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++)
+			atomic64_set(&device->scrub_stat_values[i], 0);
+		device->scrub_stats_valid = 1;
+		btrfs_release_path(path);
+		return ret < 0 ? ret : 0;
+	}
+
+	slot = path->slots[0];
+	eb = path->nodes[0];
+	item_size = btrfs_item_size(eb, slot);
+	ptr = btrfs_item_ptr(eb, slot, struct btrfs_scrub_stats_item);
+
+	for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++) {
+		u64 val = 0;
+
+		if (item_size >= (i + 1) * sizeof(__le64))
+			val = btrfs_scrub_stats_value(eb, ptr, i);
+		atomic64_set(&device->scrub_stat_values[i], (long long)val);
+	}
+
+	device->scrub_stats_valid = 1;
+	btrfs_release_path(path);
+	return 0;
+}
+
+int btrfs_init_scrub_stats(struct btrfs_fs_info *fs_info)
+{
+	struct btrfs_fs_devices *fs_devices = fs_info->fs_devices;
+	struct btrfs_device *device;
+	int ret = 0;
+
+	BTRFS_PATH_AUTO_FREE(path);
+
+	path = btrfs_alloc_path();
+	if (!path)
+		return -ENOMEM;
+
+	mutex_lock(&fs_devices->device_list_mutex);
+	list_for_each_entry(device, &fs_devices->devices, dev_list) {
+		ret = btrfs_device_init_scrub_stats(device, path);
+		if (ret)
+			goto out;
+	}
+out:
+	mutex_unlock(&fs_devices->device_list_mutex);
+	return ret;
+}
+
+static int update_scrub_stat_item(struct btrfs_trans_handle *trans,
+				  struct btrfs_device *device)
+{
+	struct btrfs_fs_info *fs_info = trans->fs_info;
+	struct btrfs_root *dev_root = fs_info->dev_root;
+	struct btrfs_key key;
+	struct extent_buffer *eb;
+	struct btrfs_scrub_stats_item *ptr;
+	int ret;
+	int i;
+
+	BTRFS_PATH_AUTO_FREE(path);
+
+	key.objectid = BTRFS_SCRUB_STATS_OBJECTID;
+	key.type = BTRFS_PERSISTENT_ITEM_KEY;
+	key.offset = device->devid;
+
+	path = btrfs_alloc_path();
+	if (!path)
+		return -ENOMEM;
+
+	ret = btrfs_search_slot(trans, dev_root, &key, path, -1, 1);
+	if (ret < 0) {
+		btrfs_warn(fs_info,
+			"error %d searching for scrub_stats item for device %s",
+			ret, btrfs_dev_name(device));
+		return ret;
+	}
+
+	if (ret == 0 &&
+	    btrfs_item_size(path->nodes[0], path->slots[0]) < sizeof(*ptr)) {
+		ret = btrfs_del_item(trans, dev_root, path);
+		if (ret) {
+			btrfs_warn(fs_info,
+				"delete undersized scrub_stats item for device %s failed %d",
+				btrfs_dev_name(device), ret);
+			return ret;
+		}
+		ret = 1;
+	}
+
+	if (ret == 1) {
+		btrfs_release_path(path);
+		ret = btrfs_insert_empty_item(trans, dev_root, path,
+					      &key, sizeof(*ptr));
+		if (ret < 0) {
+			btrfs_warn(fs_info,
+				"insert scrub_stats item for device %s failed %d",
+				btrfs_dev_name(device), ret);
+			return ret;
+		}
+	}
+
+	eb = path->nodes[0];
+	ptr = btrfs_item_ptr(eb, path->slots[0], struct btrfs_scrub_stats_item);
+	for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++)
+		btrfs_set_scrub_stats_value(eb, ptr, i,
+					    btrfs_scrub_stat_read(device, i));
+	return 0;
+}
+
+/*
+ * Called from commit_transaction.  Flushes changed scrub lifetime stats to
+ * disk.  Mirrors btrfs_run_dev_stats().
+ */
+int btrfs_run_scrub_stats(struct btrfs_trans_handle *trans)
+{
+	struct btrfs_fs_info *fs_info = trans->fs_info;
+	struct btrfs_fs_devices *fs_devices = fs_info->fs_devices;
+	struct btrfs_device *device;
+	int stats_cnt;
+	int ret = 0;
+
+	mutex_lock(&fs_devices->device_list_mutex);
+	list_for_each_entry(device, &fs_devices->devices, dev_list) {
+		stats_cnt = atomic_read(&device->scrub_stats_ccnt);
+		if (!device->scrub_stats_valid || stats_cnt == 0)
+			continue;
+
+		/*
+		 * LOAD-LOAD control dependency: reading scrub_stats_ccnt before
+		 * the counter values requires an explicit read barrier.  Pairs
+		 * with smp_mb__before_atomic() in btrfs_scrub_stat_add/set.
+		 */
+		smp_rmb();
+
+		ret = update_scrub_stat_item(trans, device);
+		if (ret)
+			break;
+		atomic_sub(stats_cnt, &device->scrub_stats_ccnt);
+	}
+	mutex_unlock(&fs_devices->device_list_mutex);
+
+	return ret;
+}
+
+/*
+ * Update per-device scrub stats after a scrub run completes (or is canceled).
+ * Accumulates session counters into the lifetime totals and records session
+ * metadata (timestamps, status, last_physical).
+ *
+ * @scrub_ret: return value from btrfs_scrub_dev(); 0=finished, -ECANCELED=
+ *             canceled, other nonzero = error/incomplete.
+ */
+void btrfs_update_scrub_stats(struct btrfs_device *dev,
+			      const struct btrfs_scrub_progress *progress,
+			      int scrub_ret)
+{
+	int i;
+	static const u64 offsets[BTRFS_SCRUB_STAT_VALUES_MAX] = {
+#define OFF(field) offsetof(struct btrfs_scrub_progress, field)
+		[BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED]  = OFF(data_extents_scrubbed),
+		[BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED]  = OFF(tree_extents_scrubbed),
+		[BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED]    = OFF(data_bytes_scrubbed),
+		[BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED]    = OFF(tree_bytes_scrubbed),
+		[BTRFS_SCRUB_STAT_READ_ERRORS]            = OFF(read_errors),
+		[BTRFS_SCRUB_STAT_CSUM_ERRORS]            = OFF(csum_errors),
+		[BTRFS_SCRUB_STAT_VERIFY_ERRORS]          = OFF(verify_errors),
+		[BTRFS_SCRUB_STAT_NO_CSUM]                = OFF(no_csum),
+		[BTRFS_SCRUB_STAT_CSUM_DISCARDS]          = OFF(csum_discards),
+		[BTRFS_SCRUB_STAT_SUPER_ERRORS]           = OFF(super_errors),
+		[BTRFS_SCRUB_STAT_MALLOC_ERRORS]          = OFF(malloc_errors),
+		[BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS]   = OFF(uncorrectable_errors),
+		[BTRFS_SCRUB_STAT_CORRECTED_ERRORS]       = OFF(corrected_errors),
+		[BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS]      = OFF(unverified_errors),
+#undef OFF
+	};
+
+	/* Update session counters from this run's progress struct */
+	for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++) {
+		u64 val = *(const u64 *)((const u8 *)progress + offsets[i]);
+
+		atomic64_set(&dev->scrub_session_values[i], (long long)val);
+		/* Accumulate into lifetime totals */
+		btrfs_scrub_stat_add(dev, i, val);
+	}
+
+	dev->scrub_session_last_physical = progress->last_physical;
+	dev->scrub_session_t_end = ktime_get_real_seconds();
+
+	if (scrub_ret == -ECANCELED)
+		atomic_set(&dev->scrub_session_status, BTRFS_SCRUB_STATUS_CANCELED);
+	else
+		atomic_set(&dev->scrub_session_status, BTRFS_SCRUB_STATUS_FINISHED);
+}
+
 /*
  * Update the size and bytes used for each device where it changed.  This is
  * delayed since we would otherwise get errors while writing out the
@@ -8609,7 +8846,7 @@ int btrfs_verify_dev_extents(struct btrfs_fs_info *fs_info)
 
 		btrfs_item_key_to_cpu(leaf, &key, slot);
 		if (key.type != BTRFS_DEV_EXTENT_KEY)
-			break;
+			goto next;
 		devid = key.objectid;
 		physical_offset = key.offset;
 
@@ -8631,7 +8868,7 @@ int btrfs_verify_dev_extents(struct btrfs_fs_info *fs_info)
 			return ret;
 		prev_devid = devid;
 		prev_dev_ext_end = physical_offset + physical_len;
-
+next:
 		ret = btrfs_next_item(root, path);
 		if (ret < 0)
 			return ret;
-- 
2.48.1


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

* [PATCH 4/5] btrfs: hook scrub session tracking into btrfs_scrub_dev()
  2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
                   ` (2 preceding siblings ...)
  2026-04-19 14:26 ` [PATCH 3/5] btrfs: persist scrub lifetime stats to the device tree Torstein Eide
@ 2026-04-19 14:26 ` Torstein Eide
  2026-04-19 14:26 ` [PATCH 5/5] btrfs: expose scrub lifetime and session counters via sysfs Torstein Eide
  4 siblings, 0 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

At the start of a scrub (excluding device replace runs):
  - zero all per-device scrub_session_values[] counters
  - set scrub_session_last_physical to the start offset
  - record scrub_session_t_start (wall-clock seconds)
  - set scrub_session_status to BTRFS_SCRUB_STATUS_RUNNING

At the end of a scrub, call btrfs_update_scrub_stats() which copies
the final btrfs_scrub_progress counters into the session and lifetime
arrays, stamps t_end, and sets the status to FINISHED or CANCELED
depending on the return value.

Device replace runs are excluded because their progress is tracked
separately and their statistics should not pollute the device's own
scrub history.

Signed-off-by: Torstein Eide <torsteine@gmail.com>
Assisted-by: Claude:claude-sonnet-4-6
---
 fs/btrfs/scrub.c | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c
index 1ac609239cbe3..a3bf3276f5f44 100644
--- a/fs/btrfs/scrub.c
+++ b/fs/btrfs/scrub.c
@@ -6,6 +6,7 @@
 #include <linux/blkdev.h>
 #include <linux/ratelimit.h>
 #include <linux/sched/mm.h>
+#include <linux/timekeeping.h>
 #include "ctree.h"
 #include "discard.h"
 #include "volumes.h"
@@ -3152,6 +3153,18 @@ int btrfs_scrub_dev(struct btrfs_fs_info *fs_info, u64 devid, u64 start,
 	dev->scrub_ctx = sctx;
 	mutex_unlock(&fs_info->fs_devices->device_list_mutex);
 
+	if (!is_dev_replace) {
+		int i;
+
+		/* Reset session counters for this new scrub run */
+		for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++)
+			atomic64_set(&dev->scrub_session_values[i], 0);
+		dev->scrub_session_last_physical = start;
+		dev->scrub_session_t_start = ktime_get_real_seconds();
+		dev->scrub_session_t_end = 0;
+		atomic_set(&dev->scrub_session_status, BTRFS_SCRUB_STATUS_RUNNING);
+	}
+
 	/*
 	 * checking @scrub_pause_req here, we can avoid
 	 * race between committing transaction and scrubbing.
@@ -3207,6 +3220,9 @@ int btrfs_scrub_dev(struct btrfs_fs_info *fs_info, u64 devid, u64 start,
 	if (progress)
 		memcpy(progress, &sctx->stat, sizeof(*progress));
 
+	if (!is_dev_replace)
+		btrfs_update_scrub_stats(dev, &sctx->stat, ret);
+
 	if (!is_dev_replace)
 		btrfs_info(fs_info, "scrub: %s on devid %llu with status: %d",
 			ret ? "not finished" : "finished", devid, ret);
-- 
2.48.1


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

* [PATCH 5/5] btrfs: expose scrub lifetime and session counters via sysfs
  2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
                   ` (3 preceding siblings ...)
  2026-04-19 14:26 ` [PATCH 4/5] btrfs: hook scrub session tracking into btrfs_scrub_dev() Torstein Eide
@ 2026-04-19 14:26 ` Torstein Eide
  4 siblings, 0 replies; 6+ messages in thread
From: Torstein Eide @ 2026-04-19 14:26 UTC (permalink / raw)
  To: linux-btrfs; +Cc: Torstein Eide

From: Torstein Eide <torsteine@gmail.com>

Create two sysfs sub-hierarchies under each filesystem UUID and under
each devinfo/<devid>/ entry:

  /sys/fs/btrfs/<UUID>/scrub/
    reset                   (write "1" to zero all devices' lifetime
                             counters and mark them dirty)
    lifetime/               filesystem-wide sums of per-device lifetime
      data_extents_scrubbed   counters (one file per stat index)
      tree_extents_scrubbed
      ... (14 counters total)
    session/                filesystem-wide view of the most recent run
      data_extents_scrubbed   (summed across devices for counters,
      ...                      earliest t_start / latest t_end for time)
      status                  idle | running | finished | canceled
      t_start / t_end         Unix timestamps (seconds)
      duration_seconds        computed on read; live while running

  /sys/fs/btrfs/<UUID>/devinfo/<devid>/scrub/
    reset                   (per-device reset)
    lifetime/               per-device lifetime counters
    session/                per-device session counters + metadata
      last_physical           byte offset where the last run stopped

The attribute arrays are declared as 'const struct attribute *[]' so
they are compatible with sysfs_create_files() / sysfs_remove_files().
Forward declarations are placed before btrfs_sysfs_remove_mounted() and
btrfs_sysfs_remove_device() which reference them before their
definitions.

Signed-off-by: Torstein Eide <torsteine@gmail.com>
Assisted-by: Claude:claude-sonnet-4-6
---
 fs/btrfs/sysfs.c | 586 +++++++++++++++++++++++++++++++++++++++++++++++
 fs/btrfs/sysfs.h |   3 +
 2 files changed, 589 insertions(+)

diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c
index 0d14570c8bc29..d5f12a40b31fc 100644
--- a/fs/btrfs/sysfs.c
+++ b/fs/btrfs/sysfs.c
@@ -11,6 +11,7 @@
 #include <linux/bug.h>
 #include <linux/list.h>
 #include <linux/string_choices.h>
+#include <linux/timekeeping.h>
 #include "messages.h"
 #include "ctree.h"
 #include "discard.h"
@@ -1707,6 +1708,14 @@ static void btrfs_sysfs_remove_fs_devices(struct btrfs_fs_devices *fs_devices)
 	}
 }
 
+/* Forward declarations for scrub sysfs attribute arrays defined later. */
+static const struct attribute *scrub_attrs[];
+static const struct attribute *scrub_lifetime_attrs[];
+static const struct attribute *scrub_session_attrs[];
+static const struct attribute *devid_scrub_attrs[];
+static const struct attribute *devid_scrub_lifetime_attrs[];
+static const struct attribute *devid_scrub_session_attrs[];
+
 void btrfs_sysfs_remove_mounted(struct btrfs_fs_info *fs_info)
 {
 	struct kobject *fsid_kobj = &fs_info->fs_devices->fsid_kobj;
@@ -1723,6 +1732,21 @@ void btrfs_sysfs_remove_mounted(struct btrfs_fs_info *fs_info)
 		kobject_del(fs_info->discard_kobj);
 		kobject_put(fs_info->discard_kobj);
 	}
+	if (fs_info->scrub_session_kobj) {
+		sysfs_remove_files(fs_info->scrub_session_kobj, scrub_session_attrs);
+		kobject_del(fs_info->scrub_session_kobj);
+		kobject_put(fs_info->scrub_session_kobj);
+	}
+	if (fs_info->scrub_lifetime_kobj) {
+		sysfs_remove_files(fs_info->scrub_lifetime_kobj, scrub_lifetime_attrs);
+		kobject_del(fs_info->scrub_lifetime_kobj);
+		kobject_put(fs_info->scrub_lifetime_kobj);
+	}
+	if (fs_info->scrub_kobj) {
+		sysfs_remove_files(fs_info->scrub_kobj, scrub_attrs);
+		kobject_del(fs_info->scrub_kobj);
+		kobject_put(fs_info->scrub_kobj);
+	}
 #ifdef CONFIG_BTRFS_DEBUG
 	if (fs_info->debug_kobj) {
 		sysfs_remove_files(fs_info->debug_kobj, btrfs_debug_mount_attrs);
@@ -1970,6 +1994,28 @@ void btrfs_sysfs_remove_device(struct btrfs_device *device)
 	if (device->bdev)
 		sysfs_remove_link(devices_kobj, bdev_kobj(device->bdev)->name);
 
+	/* Tear down devinfo/<devid>/scrub/ hierarchy (children before parent) */
+	if (device->scrub_session_kobj) {
+		sysfs_remove_files(device->scrub_session_kobj,
+				   devid_scrub_session_attrs);
+		kobject_del(device->scrub_session_kobj);
+		kobject_put(device->scrub_session_kobj);
+		device->scrub_session_kobj = NULL;
+	}
+	if (device->scrub_lifetime_kobj) {
+		sysfs_remove_files(device->scrub_lifetime_kobj,
+				   devid_scrub_lifetime_attrs);
+		kobject_del(device->scrub_lifetime_kobj);
+		kobject_put(device->scrub_lifetime_kobj);
+		device->scrub_lifetime_kobj = NULL;
+	}
+	if (device->scrub_kobj) {
+		sysfs_remove_files(device->scrub_kobj, devid_scrub_attrs);
+		kobject_del(device->scrub_kobj);
+		kobject_put(device->scrub_kobj);
+		device->scrub_kobj = NULL;
+	}
+
 	if (device->devid_kobj.state_initialized) {
 		kobject_del(&device->devid_kobj);
 		kobject_put(&device->devid_kobj);
@@ -2099,6 +2145,482 @@ static ssize_t btrfs_devinfo_error_stats_show(struct kobject *kobj,
 }
 BTRFS_ATTR(devid, error_stats, btrfs_devinfo_error_stats_show);
 
+/* ---------- scrub/lifetime/ and scrub/session/ sysfs attributes ---------- */
+
+/*
+ * Return the btrfs_device owning a kobject that lives two levels below
+ * devid_kobj, i.e. kobj->parent is scrub_kobj, kobj->parent->parent is
+ * devid_kobj.
+ */
+static struct btrfs_device *kobj_to_scrub_device(struct kobject *kobj)
+{
+	return container_of(kobj->parent->parent, struct btrfs_device, devid_kobj);
+}
+
+/*
+ * Return the btrfs_fs_info owning a kobject that lives two levels below
+ * fsid_kobj, i.e. kobj->parent is fs_info->scrub_kobj,
+ * kobj->parent->parent is &fs_devs->fsid_kobj.
+ */
+static struct btrfs_fs_info *kobj_to_scrub_fs_info(struct kobject *kobj)
+{
+	struct btrfs_fs_devices *fs_devs =
+		container_of(kobj->parent->parent, struct btrfs_fs_devices, fsid_kobj);
+	return fs_devs->fs_info;
+}
+
+/*
+ * Per-device scrub/lifetime/ attributes.
+ * Each show function reads an atomic64 from btrfs_device::scrub_stat_values[].
+ */
+#define DEV_SCRUB_LIFETIME_ATTR(_name, _idx)					\
+static ssize_t btrfs_devid_scrub_lifetime_##_name##_show(			\
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)	\
+{										\
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);			\
+	return sysfs_emit(buf, "%llu\n", btrfs_scrub_stat_read(dev, _idx));	\
+}										\
+BTRFS_ATTR(devid_scrub_lifetime, _name,					\
+	   btrfs_devid_scrub_lifetime_##_name##_show)
+
+DEV_SCRUB_LIFETIME_ATTR(data_extents_scrubbed, BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED);
+DEV_SCRUB_LIFETIME_ATTR(tree_extents_scrubbed, BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED);
+DEV_SCRUB_LIFETIME_ATTR(data_bytes_scrubbed,   BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED);
+DEV_SCRUB_LIFETIME_ATTR(tree_bytes_scrubbed,   BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED);
+DEV_SCRUB_LIFETIME_ATTR(read_errors,           BTRFS_SCRUB_STAT_READ_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(csum_errors,           BTRFS_SCRUB_STAT_CSUM_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(verify_errors,         BTRFS_SCRUB_STAT_VERIFY_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(no_csum,               BTRFS_SCRUB_STAT_NO_CSUM);
+DEV_SCRUB_LIFETIME_ATTR(csum_discards,         BTRFS_SCRUB_STAT_CSUM_DISCARDS);
+DEV_SCRUB_LIFETIME_ATTR(super_errors,          BTRFS_SCRUB_STAT_SUPER_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(malloc_errors,         BTRFS_SCRUB_STAT_MALLOC_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(uncorrectable_errors,  BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(corrected_errors,      BTRFS_SCRUB_STAT_CORRECTED_ERRORS);
+DEV_SCRUB_LIFETIME_ATTR(unverified_errors,     BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS);
+
+static const struct attribute *devid_scrub_lifetime_attrs[] = {
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, data_extents_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, tree_extents_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, data_bytes_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, tree_bytes_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, read_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, csum_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, verify_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, no_csum),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, csum_discards),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, super_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, malloc_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, uncorrectable_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, corrected_errors),
+	BTRFS_ATTR_PTR(devid_scrub_lifetime, unverified_errors),
+	NULL
+};
+
+/*
+ * Per-device scrub/session/ attributes.
+ */
+#define DEV_SCRUB_SESSION_ATTR(_name, _idx)					\
+static ssize_t btrfs_devid_scrub_session_##_name##_show(			\
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)	\
+{										\
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);			\
+	return sysfs_emit(buf, "%llu\n", btrfs_scrub_session_read(dev, _idx));	\
+}										\
+BTRFS_ATTR(devid_scrub_session, _name,					\
+	   btrfs_devid_scrub_session_##_name##_show)
+
+DEV_SCRUB_SESSION_ATTR(data_extents_scrubbed, BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED);
+DEV_SCRUB_SESSION_ATTR(tree_extents_scrubbed, BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED);
+DEV_SCRUB_SESSION_ATTR(data_bytes_scrubbed,   BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED);
+DEV_SCRUB_SESSION_ATTR(tree_bytes_scrubbed,   BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED);
+DEV_SCRUB_SESSION_ATTR(read_errors,           BTRFS_SCRUB_STAT_READ_ERRORS);
+DEV_SCRUB_SESSION_ATTR(csum_errors,           BTRFS_SCRUB_STAT_CSUM_ERRORS);
+DEV_SCRUB_SESSION_ATTR(verify_errors,         BTRFS_SCRUB_STAT_VERIFY_ERRORS);
+DEV_SCRUB_SESSION_ATTR(no_csum,               BTRFS_SCRUB_STAT_NO_CSUM);
+DEV_SCRUB_SESSION_ATTR(csum_discards,         BTRFS_SCRUB_STAT_CSUM_DISCARDS);
+DEV_SCRUB_SESSION_ATTR(super_errors,          BTRFS_SCRUB_STAT_SUPER_ERRORS);
+DEV_SCRUB_SESSION_ATTR(malloc_errors,         BTRFS_SCRUB_STAT_MALLOC_ERRORS);
+DEV_SCRUB_SESSION_ATTR(uncorrectable_errors,  BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS);
+DEV_SCRUB_SESSION_ATTR(corrected_errors,      BTRFS_SCRUB_STAT_CORRECTED_ERRORS);
+DEV_SCRUB_SESSION_ATTR(unverified_errors,     BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS);
+
+static ssize_t btrfs_devid_scrub_session_last_physical_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);
+
+	return sysfs_emit(buf, "%llu\n", READ_ONCE(dev->scrub_session_last_physical));
+}
+BTRFS_ATTR(devid_scrub_session, last_physical,
+	   btrfs_devid_scrub_session_last_physical_show);
+
+static ssize_t btrfs_devid_scrub_session_t_start_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);
+
+	return sysfs_emit(buf, "%llu\n", READ_ONCE(dev->scrub_session_t_start));
+}
+BTRFS_ATTR(devid_scrub_session, t_start, btrfs_devid_scrub_session_t_start_show);
+
+static ssize_t btrfs_devid_scrub_session_t_end_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);
+
+	return sysfs_emit(buf, "%llu\n", READ_ONCE(dev->scrub_session_t_end));
+}
+BTRFS_ATTR(devid_scrub_session, t_end, btrfs_devid_scrub_session_t_end_show);
+
+static ssize_t btrfs_devid_scrub_session_duration_seconds_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);
+	u64 t_start = READ_ONCE(dev->scrub_session_t_start);
+	u64 t_end   = READ_ONCE(dev->scrub_session_t_end);
+	u64 dur;
+
+	if (t_start == 0)
+		dur = 0;
+	else if (t_end != 0)
+		dur = t_end - t_start;
+	else
+		dur = (u64)ktime_get_real_seconds() - t_start;
+	return sysfs_emit(buf, "%llu\n", dur);
+}
+BTRFS_ATTR(devid_scrub_session, duration_seconds,
+	   btrfs_devid_scrub_session_duration_seconds_show);
+
+static const char * const btrfs_scrub_status_strings[] = {
+	[BTRFS_SCRUB_STATUS_IDLE]     = "idle",
+	[BTRFS_SCRUB_STATUS_RUNNING]  = "running",
+	[BTRFS_SCRUB_STATUS_FINISHED] = "finished",
+	[BTRFS_SCRUB_STATUS_CANCELED] = "canceled",
+};
+
+static ssize_t btrfs_devid_scrub_session_status_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_device *dev = kobj_to_scrub_device(kobj);
+	int status = atomic_read(&dev->scrub_session_status);
+
+	if (status < 0 || status >= ARRAY_SIZE(btrfs_scrub_status_strings))
+		return sysfs_emit(buf, "unknown\n");
+	return sysfs_emit(buf, "%s\n", btrfs_scrub_status_strings[status]);
+}
+BTRFS_ATTR(devid_scrub_session, status, btrfs_devid_scrub_session_status_show);
+
+static const struct attribute *devid_scrub_session_attrs[] = {
+	BTRFS_ATTR_PTR(devid_scrub_session, data_extents_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_session, tree_extents_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_session, data_bytes_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_session, tree_bytes_scrubbed),
+	BTRFS_ATTR_PTR(devid_scrub_session, read_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, csum_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, verify_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, no_csum),
+	BTRFS_ATTR_PTR(devid_scrub_session, csum_discards),
+	BTRFS_ATTR_PTR(devid_scrub_session, super_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, malloc_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, uncorrectable_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, corrected_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, unverified_errors),
+	BTRFS_ATTR_PTR(devid_scrub_session, last_physical),
+	BTRFS_ATTR_PTR(devid_scrub_session, t_start),
+	BTRFS_ATTR_PTR(devid_scrub_session, t_end),
+	BTRFS_ATTR_PTR(devid_scrub_session, duration_seconds),
+	BTRFS_ATTR_PTR(devid_scrub_session, status),
+	NULL
+};
+
+/*
+ * Per-device scrub/reset (write-only): "echo 1 > reset" zeroes lifetime
+ * counters and marks them dirty for the next transaction commit.
+ */
+static ssize_t btrfs_devid_scrub_reset_store(struct kobject *kobj,
+		struct kobj_attribute *a, const char *buf, size_t len)
+{
+	struct btrfs_device *dev;
+	unsigned long val;
+	int i;
+
+	if (kstrtoul(buf, 10, &val) || val != 1)
+		return -EINVAL;
+
+	/* kobj here is the scrub_kobj, one level below devid_kobj */
+	dev = container_of(kobj->parent, struct btrfs_device, devid_kobj);
+
+	for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++)
+		btrfs_scrub_stat_set(dev, i, 0);
+
+	btrfs_info(dev->fs_info,
+		   "scrub: lifetime stats reset for devid %llu by %s (%d)",
+		   dev->devid, current->comm, task_pid_nr(current));
+	return len;
+}
+BTRFS_ATTR_W(devid_scrub, reset, btrfs_devid_scrub_reset_store);
+
+static const struct attribute *devid_scrub_attrs[] = {
+	BTRFS_ATTR_PTR(devid_scrub, reset),
+	NULL
+};
+
+/* ---------- fs-level scrub/lifetime/ and scrub/session/ attributes ---------- */
+
+/*
+ * Filesystem-level lifetime counters: sum of all device lifetime counters.
+ */
+#define FS_SCRUB_LIFETIME_ATTR(_name, _idx)					\
+static ssize_t btrfs_scrub_lifetime_##_name##_show(				\
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)	\
+{										\
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);		\
+	struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;			\
+	struct btrfs_device *dev;						\
+	u64 total = 0;								\
+										\
+	mutex_lock(&fs_devs->device_list_mutex);				\
+	list_for_each_entry(dev, &fs_devs->devices, dev_list)			\
+		total += btrfs_scrub_stat_read(dev, _idx);			\
+	mutex_unlock(&fs_devs->device_list_mutex);				\
+	return sysfs_emit(buf, "%llu\n", total);				\
+}										\
+BTRFS_ATTR(scrub_lifetime, _name, btrfs_scrub_lifetime_##_name##_show)
+
+FS_SCRUB_LIFETIME_ATTR(data_extents_scrubbed, BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED);
+FS_SCRUB_LIFETIME_ATTR(tree_extents_scrubbed, BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED);
+FS_SCRUB_LIFETIME_ATTR(data_bytes_scrubbed,   BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED);
+FS_SCRUB_LIFETIME_ATTR(tree_bytes_scrubbed,   BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED);
+FS_SCRUB_LIFETIME_ATTR(read_errors,           BTRFS_SCRUB_STAT_READ_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(csum_errors,           BTRFS_SCRUB_STAT_CSUM_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(verify_errors,         BTRFS_SCRUB_STAT_VERIFY_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(no_csum,               BTRFS_SCRUB_STAT_NO_CSUM);
+FS_SCRUB_LIFETIME_ATTR(csum_discards,         BTRFS_SCRUB_STAT_CSUM_DISCARDS);
+FS_SCRUB_LIFETIME_ATTR(super_errors,          BTRFS_SCRUB_STAT_SUPER_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(malloc_errors,         BTRFS_SCRUB_STAT_MALLOC_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(uncorrectable_errors,  BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(corrected_errors,      BTRFS_SCRUB_STAT_CORRECTED_ERRORS);
+FS_SCRUB_LIFETIME_ATTR(unverified_errors,     BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS);
+
+static const struct attribute *scrub_lifetime_attrs[] = {
+	BTRFS_ATTR_PTR(scrub_lifetime, data_extents_scrubbed),
+	BTRFS_ATTR_PTR(scrub_lifetime, tree_extents_scrubbed),
+	BTRFS_ATTR_PTR(scrub_lifetime, data_bytes_scrubbed),
+	BTRFS_ATTR_PTR(scrub_lifetime, tree_bytes_scrubbed),
+	BTRFS_ATTR_PTR(scrub_lifetime, read_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, csum_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, verify_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, no_csum),
+	BTRFS_ATTR_PTR(scrub_lifetime, csum_discards),
+	BTRFS_ATTR_PTR(scrub_lifetime, super_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, malloc_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, uncorrectable_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, corrected_errors),
+	BTRFS_ATTR_PTR(scrub_lifetime, unverified_errors),
+	NULL
+};
+
+/*
+ * Filesystem-level session counters: sum of per-device session values.
+ * Timing/status derive from per-device values.
+ */
+#define FS_SCRUB_SESSION_ATTR(_name, _idx)					\
+static ssize_t btrfs_scrub_session_##_name##_show(				\
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)	\
+{										\
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);		\
+	struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;			\
+	struct btrfs_device *dev;						\
+	u64 total = 0;								\
+										\
+	mutex_lock(&fs_devs->device_list_mutex);				\
+	list_for_each_entry(dev, &fs_devs->devices, dev_list)			\
+		total += btrfs_scrub_session_read(dev, _idx);			\
+	mutex_unlock(&fs_devs->device_list_mutex);				\
+	return sysfs_emit(buf, "%llu\n", total);				\
+}										\
+BTRFS_ATTR(scrub_session, _name, btrfs_scrub_session_##_name##_show)
+
+FS_SCRUB_SESSION_ATTR(data_extents_scrubbed, BTRFS_SCRUB_STAT_DATA_EXTENTS_SCRUBBED);
+FS_SCRUB_SESSION_ATTR(tree_extents_scrubbed, BTRFS_SCRUB_STAT_TREE_EXTENTS_SCRUBBED);
+FS_SCRUB_SESSION_ATTR(data_bytes_scrubbed,   BTRFS_SCRUB_STAT_DATA_BYTES_SCRUBBED);
+FS_SCRUB_SESSION_ATTR(tree_bytes_scrubbed,   BTRFS_SCRUB_STAT_TREE_BYTES_SCRUBBED);
+FS_SCRUB_SESSION_ATTR(read_errors,           BTRFS_SCRUB_STAT_READ_ERRORS);
+FS_SCRUB_SESSION_ATTR(csum_errors,           BTRFS_SCRUB_STAT_CSUM_ERRORS);
+FS_SCRUB_SESSION_ATTR(verify_errors,         BTRFS_SCRUB_STAT_VERIFY_ERRORS);
+FS_SCRUB_SESSION_ATTR(no_csum,               BTRFS_SCRUB_STAT_NO_CSUM);
+FS_SCRUB_SESSION_ATTR(csum_discards,         BTRFS_SCRUB_STAT_CSUM_DISCARDS);
+FS_SCRUB_SESSION_ATTR(super_errors,          BTRFS_SCRUB_STAT_SUPER_ERRORS);
+FS_SCRUB_SESSION_ATTR(malloc_errors,         BTRFS_SCRUB_STAT_MALLOC_ERRORS);
+FS_SCRUB_SESSION_ATTR(uncorrectable_errors,  BTRFS_SCRUB_STAT_UNCORRECTABLE_ERRORS);
+FS_SCRUB_SESSION_ATTR(corrected_errors,      BTRFS_SCRUB_STAT_CORRECTED_ERRORS);
+FS_SCRUB_SESSION_ATTR(unverified_errors,     BTRFS_SCRUB_STAT_UNVERIFIED_ERRORS);
+
+static ssize_t btrfs_scrub_session_status_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);
+
+	if (atomic_read(&fs_info->scrubs_running) > 0)
+		return sysfs_emit(buf, "running\n");
+
+	/*
+	 * Not running: check if any device's last session finished or was
+	 * canceled.
+	 */
+	{
+		struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;
+		struct btrfs_device *dev;
+		int seen_finished = 0, seen_canceled = 0;
+
+		mutex_lock(&fs_devs->device_list_mutex);
+		list_for_each_entry(dev, &fs_devs->devices, dev_list) {
+			int st = atomic_read(&dev->scrub_session_status);
+
+			if (st == BTRFS_SCRUB_STATUS_FINISHED)
+				seen_finished = 1;
+			else if (st == BTRFS_SCRUB_STATUS_CANCELED)
+				seen_canceled = 1;
+		}
+		mutex_unlock(&fs_devs->device_list_mutex);
+
+		if (seen_canceled)
+			return sysfs_emit(buf, "canceled\n");
+		if (seen_finished)
+			return sysfs_emit(buf, "finished\n");
+	}
+	return sysfs_emit(buf, "idle\n");
+}
+BTRFS_ATTR(scrub_session, status, btrfs_scrub_session_status_show);
+
+static ssize_t btrfs_scrub_session_t_start_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);
+	struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;
+	struct btrfs_device *dev;
+	u64 t_min = 0;
+
+	mutex_lock(&fs_devs->device_list_mutex);
+	list_for_each_entry(dev, &fs_devs->devices, dev_list) {
+		u64 t = READ_ONCE(dev->scrub_session_t_start);
+
+		if (t && (!t_min || t < t_min))
+			t_min = t;
+	}
+	mutex_unlock(&fs_devs->device_list_mutex);
+	return sysfs_emit(buf, "%llu\n", t_min);
+}
+BTRFS_ATTR(scrub_session, t_start, btrfs_scrub_session_t_start_show);
+
+static ssize_t btrfs_scrub_session_t_end_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);
+	struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;
+	struct btrfs_device *dev;
+	u64 t_max = 0;
+
+	mutex_lock(&fs_devs->device_list_mutex);
+	list_for_each_entry(dev, &fs_devs->devices, dev_list) {
+		u64 t = READ_ONCE(dev->scrub_session_t_end);
+
+		if (t > t_max)
+			t_max = t;
+	}
+	mutex_unlock(&fs_devs->device_list_mutex);
+	return sysfs_emit(buf, "%llu\n", t_max);
+}
+BTRFS_ATTR(scrub_session, t_end, btrfs_scrub_session_t_end_show);
+
+static ssize_t btrfs_scrub_session_duration_seconds_show(
+		struct kobject *kobj, struct kobj_attribute *a, char *buf)
+{
+	struct btrfs_fs_info *fs_info = kobj_to_scrub_fs_info(kobj);
+	struct btrfs_fs_devices *fs_devs = fs_info->fs_devices;
+	struct btrfs_device *dev;
+	u64 t_start = 0, t_end = 0, dur;
+
+	mutex_lock(&fs_devs->device_list_mutex);
+	list_for_each_entry(dev, &fs_devs->devices, dev_list) {
+		u64 ts = READ_ONCE(dev->scrub_session_t_start);
+		u64 te = READ_ONCE(dev->scrub_session_t_end);
+
+		if (ts && (!t_start || ts < t_start))
+			t_start = ts;
+		if (te > t_end)
+			t_end = te;
+	}
+	mutex_unlock(&fs_devs->device_list_mutex);
+
+	if (!t_start)
+		dur = 0;
+	else if (t_end)
+		dur = t_end - t_start;
+	else
+		dur = (u64)ktime_get_real_seconds() - t_start;
+	return sysfs_emit(buf, "%llu\n", dur);
+}
+BTRFS_ATTR(scrub_session, duration_seconds,
+	   btrfs_scrub_session_duration_seconds_show);
+
+static const struct attribute *scrub_session_attrs[] = {
+	BTRFS_ATTR_PTR(scrub_session, data_extents_scrubbed),
+	BTRFS_ATTR_PTR(scrub_session, tree_extents_scrubbed),
+	BTRFS_ATTR_PTR(scrub_session, data_bytes_scrubbed),
+	BTRFS_ATTR_PTR(scrub_session, tree_bytes_scrubbed),
+	BTRFS_ATTR_PTR(scrub_session, read_errors),
+	BTRFS_ATTR_PTR(scrub_session, csum_errors),
+	BTRFS_ATTR_PTR(scrub_session, verify_errors),
+	BTRFS_ATTR_PTR(scrub_session, no_csum),
+	BTRFS_ATTR_PTR(scrub_session, csum_discards),
+	BTRFS_ATTR_PTR(scrub_session, super_errors),
+	BTRFS_ATTR_PTR(scrub_session, malloc_errors),
+	BTRFS_ATTR_PTR(scrub_session, uncorrectable_errors),
+	BTRFS_ATTR_PTR(scrub_session, corrected_errors),
+	BTRFS_ATTR_PTR(scrub_session, unverified_errors),
+	BTRFS_ATTR_PTR(scrub_session, status),
+	BTRFS_ATTR_PTR(scrub_session, t_start),
+	BTRFS_ATTR_PTR(scrub_session, t_end),
+	BTRFS_ATTR_PTR(scrub_session, duration_seconds),
+	NULL
+};
+
+/*
+ * Filesystem-level scrub/reset (write-only): zeros all devices' lifetime
+ * counters and marks them dirty.
+ */
+static ssize_t btrfs_scrub_reset_store(struct kobject *kobj,
+		struct kobj_attribute *a, const char *buf, size_t len)
+{
+	/* kobj is fs_info->scrub_kobj, parent is fsid_kobj */
+	struct btrfs_fs_devices *fs_devs =
+		container_of(kobj->parent, struct btrfs_fs_devices, fsid_kobj);
+	struct btrfs_fs_info *fs_info = fs_devs->fs_info;
+	struct btrfs_device *dev;
+	unsigned long val;
+	int i;
+
+	if (kstrtoul(buf, 10, &val) || val != 1)
+		return -EINVAL;
+
+	mutex_lock(&fs_devs->device_list_mutex);
+	list_for_each_entry(dev, &fs_devs->devices, dev_list)
+		for (i = 0; i < BTRFS_SCRUB_STAT_VALUES_MAX; i++)
+			btrfs_scrub_stat_set(dev, i, 0);
+	mutex_unlock(&fs_devs->device_list_mutex);
+
+	btrfs_info(fs_info, "scrub: all lifetime stats reset by %s (%d)",
+		   current->comm, task_pid_nr(current));
+	return len;
+}
+BTRFS_ATTR_W(scrub, reset, btrfs_scrub_reset_store);
+
+static const struct attribute *scrub_attrs[] = {
+	BTRFS_ATTR_PTR(scrub, reset),
+	NULL
+};
+
 /*
  * Information about one device.
  *
@@ -2169,7 +2691,41 @@ int btrfs_sysfs_add_device(struct btrfs_device *device)
 		btrfs_warn(device->fs_info,
 			   "devinfo init for devid %llu failed: %d",
 			   device->devid, ret);
+		goto out;
+	}
+
+	/* Create devinfo/<devid>/scrub/ hierarchy */
+	device->scrub_kobj = kobject_create_and_add("scrub", &device->devid_kobj);
+	if (!device->scrub_kobj) {
+		ret = -ENOMEM;
+		btrfs_warn(device->fs_info,
+			   "scrub kobj init for devid %llu failed",
+			   device->devid);
+		goto out;
+	}
+	ret = sysfs_create_files(device->scrub_kobj, devid_scrub_attrs);
+	if (ret)
+		goto out;
+
+	device->scrub_lifetime_kobj = kobject_create_and_add("lifetime",
+							      device->scrub_kobj);
+	if (!device->scrub_lifetime_kobj) {
+		ret = -ENOMEM;
+		goto out;
+	}
+	ret = sysfs_create_files(device->scrub_lifetime_kobj,
+				 devid_scrub_lifetime_attrs);
+	if (ret)
+		goto out;
+
+	device->scrub_session_kobj = kobject_create_and_add("session",
+							     device->scrub_kobj);
+	if (!device->scrub_session_kobj) {
+		ret = -ENOMEM;
+		goto out;
 	}
+	ret = sysfs_create_files(device->scrub_session_kobj,
+				 devid_scrub_session_attrs);
 
 out:
 	memalloc_nofs_restore(nofs_flag);
@@ -2346,6 +2902,36 @@ int btrfs_sysfs_add_mounted(struct btrfs_fs_info *fs_info)
 	if (ret)
 		goto failure;
 
+	/* Create /sys/fs/btrfs/<UUID>/scrub/{lifetime,session}/ hierarchy */
+	fs_info->scrub_kobj = kobject_create_and_add("scrub", fsid_kobj);
+	if (!fs_info->scrub_kobj) {
+		ret = -ENOMEM;
+		goto failure;
+	}
+	ret = sysfs_create_files(fs_info->scrub_kobj, scrub_attrs);
+	if (ret)
+		goto failure;
+
+	fs_info->scrub_lifetime_kobj = kobject_create_and_add("lifetime",
+							       fs_info->scrub_kobj);
+	if (!fs_info->scrub_lifetime_kobj) {
+		ret = -ENOMEM;
+		goto failure;
+	}
+	ret = sysfs_create_files(fs_info->scrub_lifetime_kobj, scrub_lifetime_attrs);
+	if (ret)
+		goto failure;
+
+	fs_info->scrub_session_kobj = kobject_create_and_add("session",
+							      fs_info->scrub_kobj);
+	if (!fs_info->scrub_session_kobj) {
+		ret = -ENOMEM;
+		goto failure;
+	}
+	ret = sysfs_create_files(fs_info->scrub_session_kobj, scrub_session_attrs);
+	if (ret)
+		goto failure;
+
 	return 0;
 failure:
 	btrfs_sysfs_remove_mounted(fs_info);
diff --git a/fs/btrfs/sysfs.h b/fs/btrfs/sysfs.h
index 05498e5346c39..93da8ea06659e 100644
--- a/fs/btrfs/sysfs.h
+++ b/fs/btrfs/sysfs.h
@@ -41,6 +41,9 @@ int btrfs_sysfs_add_space_info_type(struct btrfs_space_info *space_info);
 void btrfs_sysfs_remove_space_info(struct btrfs_space_info *space_info);
 void btrfs_sysfs_update_devid(struct btrfs_device *device);
 
+int btrfs_sysfs_add_scrub_device(struct btrfs_device *device);
+void btrfs_sysfs_remove_scrub_device(struct btrfs_device *device);
+
 int btrfs_sysfs_add_one_qgroup(struct btrfs_fs_info *fs_info,
 				struct btrfs_qgroup *qgroup);
 void btrfs_sysfs_del_qgroups(struct btrfs_fs_info *fs_info);
-- 
2.48.1


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

end of thread, other threads:[~2026-04-19 14:26 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-19 14:26 [PATCH 0/5] btrfs: add persistent scrub lifetime and session counters Torstein Eide
2026-04-19 14:26 ` [PATCH 1/5] btrfs: uapi: introduce on-disk scrub stats item Torstein Eide
2026-04-19 14:26 ` [PATCH 2/5] btrfs: add in-memory scrub lifetime and session fields Torstein Eide
2026-04-19 14:26 ` [PATCH 3/5] btrfs: persist scrub lifetime stats to the device tree Torstein Eide
2026-04-19 14:26 ` [PATCH 4/5] btrfs: hook scrub session tracking into btrfs_scrub_dev() Torstein Eide
2026-04-19 14:26 ` [PATCH 5/5] btrfs: expose scrub lifetime and session counters via sysfs Torstein Eide

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