diff --git a/README.md b/README.md
index 89f1bea..991a986 100644
--- a/README.md
+++ b/README.md
@@ -35,6 +35,7 @@ import {
getUiFps,
getCpuUsage,
getMemoryUsage,
+ getExtendedMemoryUsage,
getDeviceMaxRefreshRate,
getDeviceCurrentRefreshRate,
} from 'react-native-performance-toolkit'
@@ -43,6 +44,7 @@ console.log('JS FPS:', getJsFps())
console.log('UI FPS:', getUiFps())
console.log('CPU Usage:', getCpuUsage())
console.log('Memory Usage:', getMemoryUsage())
+console.log('Extended Memory Usage:', getExtendedMemoryUsage())
console.log('Max Refresh Rate:', getDeviceMaxRefreshRate(), 'Hz')
console.log('Current Refresh Rate:', getDeviceCurrentRefreshRate(), 'Hz')
```
@@ -169,6 +171,23 @@ console.log('CPU Usage:', getValueFromBuffer(cpuUsageBuffer))
console.log('Memory Usage:', getValueFromBuffer(memoryUsageBuffer))
```
+On iOS, `getMemoryUsageBuffer()` returns a 24-byte buffer so advanced consumers can also read resident size and VM region count without an extra native call:
+
+| Offset | Type | Field | Unit |
+| ------ | --------- | ---------------- | ----- |
+| 0 | `Int32` | `phys_footprint` | MB |
+| 8 | `Float64` | `resident_size` | KB |
+| 16 | `Float64` | `region_count` | count |
+
+Offset 0 stays compatible with `getMemoryUsage()`. Prefer `getExtendedMemoryUsage()` unless you need raw buffer access (e.g. from a worklet). Android currently keeps the 4-byte PSS buffer; extended fields read as `0`.
+
+```tsx
+import { getExtendedMemoryUsage } from 'react-native-performance-toolkit'
+
+const { memoryUsageMb, residentSizeKb, regionCount } =
+ getExtendedMemoryUsage()
+```
+
### Access from worklets (advanced usage)
> **Note:** This requires `react-native-reanimated` and `react-native-worklets` to be installed.
@@ -210,6 +229,7 @@ const updateFps = useCallback(() => {
- `getUiFps(): number` - Returns current UI FPS (0-30/60/90/120/...)
- `getCpuUsage(): number` - Returns CPU usage percentage in Linux format. Returns `0` on the first call (CPU usage is a rate and requires a previous sample to compute a delta); subsequent calls return real values.
- `getMemoryUsage(): number` - Returns memory usage in megabytes (MB).
+ - `getExtendedMemoryUsage(): { memoryUsageMb: number, residentSizeKb: number, regionCount: number }` - Returns extended memory metrics. `memoryUsageMb` is `phys_footprint` on iOS and PSS on Android. On iOS, the additional fields contain `task_vm_info` resident size (KB) and region count; on Android they are `0`.
- `getDeviceMaxRefreshRate(): number` - Returns device's maximum supported refresh rate (e.g., 120 Hz on ProMotion devices)
- `getDeviceCurrentRefreshRate(): number` - Returns device's current active refresh rate (may be lower than max on adaptive refresh rate displays)
@@ -218,18 +238,20 @@ const updateFps = useCallback(() => {
- `onFpsUiChange(callback: (fps: number) => void): () => void` - Subscribe to UI FPS changes
- `onCpuChange(callback: (value: number) => void): () => void` - Subscribe to CPU usage changes
- `onMemoryChange(callback: (value: number) => void): () => void` - Subscribe to memory usage changes
+ - `onExtendedMemoryChange(callback: (value: { memoryUsageMb: number, residentSizeKb: number, regionCount: number }) => void): () => void` - Subscribe to extended memory usage changes
- **React Hooks (JS Thread)**
- `useFpsJs(): number` - Hook that returns current JS FPS
- `useFpsUi(): number` - Hook that returns current UI FPS
- `useCpuUsage(): number` - Hook that returns current CPU usage
- `useMemoryUsage(): number` - Hook that returns current memory usage
+ - `useExtendedMemoryUsage(): { memoryUsageMb: number, residentSizeKb: number, regionCount: number }` - Hook that returns current extended memory usage
- **Buffer-based API**
- `getJsFpsBuffer(): ArrayBuffer` - Returns ArrayBuffer with JS FPS data
- `getUiFpsBuffer(): ArrayBuffer` - Returns ArrayBuffer with UI FPS data
- `getCpuUsageBuffer(): ArrayBuffer` - Returns ArrayBuffer with CPU usage data
- - `getMemoryUsageBuffer(): ArrayBuffer` - Returns ArrayBuffer with memory usage data
+ - `getMemoryUsageBuffer(): ArrayBuffer` - Returns ArrayBuffer with memory usage data. On iOS this is 24 bytes (`phys_footprint` MB + `resident_size` KB + `region_count`); on Android it remains 4 bytes (PSS MB).
- **Advanced (Nitro Modules)**
- `BoxedJsFpsTracking` - Direct boxed Nitro module instance for worklet usage
@@ -266,7 +288,7 @@ Import from `react-native-performance-toolkit/reanimated`:
On Android, the library is reading values from virtual files like `/proc/stat` for CPU usage and `/proc/smaps_rollup` for memory usage. This is very low overhead and doesn't require any additional permissions.
-On iOS, the library is reading values from `task_vm_info`/`rusage` direct kernel call. This is also extremely low overhead.
+On iOS, the library is reading values from `task_vm_info`/`rusage` direct kernel call. This is also extremely low overhead. The memory buffer exposes `phys_footprint` (primary), plus `resident_size` and `region_count` for deeper diagnostics.
### Device Refresh Rate
diff --git a/example/App.tsx b/example/App.tsx
index e5fe4d7..33e6143 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -4,6 +4,7 @@ import {
useFpsUi,
useCpuUsage,
useMemoryUsage,
+ useExtendedMemoryUsage,
useFpsJs,
getDeviceMaxRefreshRate,
getDeviceCurrentRefreshRate,
@@ -43,6 +44,20 @@ const MemoryUsageCounterJSThread = () => {
return RAM (MB): {formatValue(memoryUsage)};
};
+const ExtendedMemoryCounterJSThread = () => {
+ const { memoryUsageMb, residentSizeKb, regionCount } =
+ useExtendedMemoryUsage();
+ return (
+ <>
+ Ext RAM (MB): {formatValue(memoryUsageMb)}
+
+ Resident (KB): {formatValue(residentSizeKb)}
+
+ Regions: {formatValue(regionCount)}
+ >
+ );
+};
+
function App(): React.JSX.Element {
const blockJSThread = (blockTime: number = 500) => {
console.log(`Blocking JS thread for ${blockTime} milliseconds...`);
@@ -103,6 +118,7 @@ function App(): React.JSX.Element {
+
diff --git a/ios/HybridPerformanceToolkit.swift b/ios/HybridPerformanceToolkit.swift
index 77e3e86..18095f6 100755
--- a/ios/HybridPerformanceToolkit.swift
+++ b/ios/HybridPerformanceToolkit.swift
@@ -46,8 +46,21 @@ class HybridPerformanceToolkit: HybridPerformanceToolkitSpec {
private static let CPU_COLLECTION_INTERVAL: CFTimeInterval = 0.5
// Memory tracking
+ // Shared backing buffer for getMemoryUsage() and getExtendedMemoryUsage().
+ // The basic API reads only offset 0; collecting extended fields adds no
+ // additional task_info call.
+ // Buffer layout (little-endian):
+ // offset 0 : Int32 phys_footprint (MB) -- primary, jetsam-relevant
+ // offset 8 : Float64 resident_size (KB) -- resident secondary
+ // offset 16 : Float64 region_count (count) -- address-space diagnostic
+ // virtual_size is intentionally not collected (reserved address space is
+ // not a useful memory-usage signal).
+ private static let MEMORY_BUFFER_SIZE = 24
+ private static let MEMORY_PHYS_FOOTPRINT_MB_OFFSET = 0
+ private static let MEMORY_RESIDENT_SIZE_KB_OFFSET = 8
+ private static let MEMORY_REGION_COUNT_OFFSET = 16
private var memoryTimer: Timer?
- private var memoryBuffer: ArrayBuffer?
+ private var memoryMetricsBuffer: ArrayBuffer?
private var isMemoryTrackingStarting = false
private lazy var maxDeviceFps: Double = {
@@ -229,16 +242,16 @@ class HybridPerformanceToolkit: HybridPerformanceToolkitSpec {
return cpuPercentage
}
- // MARK: - Memory Usage Buffer
+ // MARK: - Memory Metrics Buffer
func getMemoryUsageBuffer() throws -> ArrayBuffer {
- if memoryBuffer == nil {
- memoryBuffer = ArrayBuffer.allocate(size: MemoryLayout.size)
- // Populate synchronously so the very first read returns a real value rather than 0.
+ if memoryMetricsBuffer == nil {
+ memoryMetricsBuffer = ArrayBuffer.allocate(size: Self.MEMORY_BUFFER_SIZE)
+ memset(memoryMetricsBuffer!.data, 0, Self.MEMORY_BUFFER_SIZE)
+ // Populate synchronously so the very first read returns real values rather than 0.
// Memory is an instantaneous measurement (no delta required), so this is safe to do
// on the calling thread; it matches the work the periodic updater does every 500ms.
- let initialRam = Int32(collectUsedRam())
- memoryBuffer!.data.withMemoryRebound(to: Int32.self, capacity: 1) { $0.pointee = initialRam }
+ updateMemoryMetricsBuffer()
}
if memoryTimer == nil && !isMemoryTrackingStarting {
@@ -246,7 +259,7 @@ class HybridPerformanceToolkit: HybridPerformanceToolkitSpec {
startMemoryTracking()
}
- return memoryBuffer!
+ return memoryMetricsBuffer!
}
private func startMemoryTracking() {
@@ -263,38 +276,94 @@ class HybridPerformanceToolkit: HybridPerformanceToolkitSpec {
}
self.memoryTimer = Timer.scheduledTimer(withTimeInterval: Self.MEMORY_UPDATE_INTERVAL, repeats: true) { [weak self] _ in
- self?.updateMemoryBuffer()
+ self?.updateMemoryMetricsBuffer()
}
self.isMemoryTrackingStarting = false
}
}
- private func updateMemoryBuffer() {
- guard let buffer = memoryBuffer else { return }
+ private struct TaskVmInfoSample {
+ let physFootprintMb: Double?
+ let residentSizeKb: Double?
+ let regionCount: Double?
+ }
+
+ private func updateMemoryMetricsBuffer() {
+ guard let buffer = memoryMetricsBuffer else { return }
- let ramValue = collectUsedRam()
+ let sample = collectTaskVmInfo()
- buffer.data.withMemoryRebound(to: Int32.self, capacity: 1) { $0.pointee = Int32(ramValue) }
+ buffer.data.advanced(by: Self.MEMORY_PHYS_FOOTPRINT_MB_OFFSET)
+ .withMemoryRebound(to: Int32.self, capacity: 1) {
+ $0.pointee = Int32(sample.physFootprintMb ?? 0.0)
+ }
+ writeDouble(buffer, offset: Self.MEMORY_RESIDENT_SIZE_KB_OFFSET, value: sample.residentSizeKb ?? 0.0)
+ writeDouble(buffer, offset: Self.MEMORY_REGION_COUNT_OFFSET, value: sample.regionCount ?? 0.0)
+ }
+
+ private func writeDouble(_ buffer: ArrayBuffer, offset: Int, value: Double) {
+ var mutableValue = value
+ withUnsafeBytes(of: &mutableValue) { bytes in
+ memcpy(buffer.data.advanced(by: offset), bytes.baseAddress!, MemoryLayout.size)
+ }
+ }
+
+ private func taskVmInfoCountCovers(_ field: KeyPath, count: mach_msg_type_number_t) -> Bool {
+ guard let offset = MemoryLayout.offset(of: field) else {
+ return false
+ }
+ let wordSize = MemoryLayout.size
+ let requiredCount = mach_msg_type_number_t((offset + MemoryLayout.size + wordSize - 1) / wordSize)
+ return count >= requiredCount
}
- private func collectUsedRam() -> Double {
- // Use task_vm_info to get phys_footprint, which matches Xcode's memory gauge
- // This excludes shared memory (frameworks, dylibs) and shows actual app footprint
+ private func collectTaskVmInfo() -> TaskVmInfoSample {
+ // TASK_VM_INFO gives phys_footprint plus resident size and region count.
+ // Swift does not import the TASK_VM_INFO_COUNT macro, so compute the same
+ // natural_t word count and verify the returned revision covers each late
+ // field before reading it. virtual_size is intentionally not read: it
+ // measures reserved-but-uncommitted address space and is useless as a
+ // memory-usage signal.
var info = task_vm_info_data_t()
- var count = mach_msg_type_number_t(MemoryLayout.size) / 4
+ var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size)
let result = withUnsafeMutablePointer(to: &info) {
- $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
+ $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
task_info(mach_task_self_, task_flavor_t(TASK_VM_INFO), $0, &count)
}
}
guard result == KERN_SUCCESS else {
- return 0.0
+ return TaskVmInfoSample(physFootprintMb: nil, residentSizeKb: nil, regionCount: nil)
+ }
+
+ let physFootprintMb: Double?
+ if taskVmInfoCountCovers(\task_vm_info_data_t.phys_footprint, count: count) {
+ physFootprintMb = Double(info.phys_footprint) / 1_048_576.0
+ } else {
+ physFootprintMb = nil
+ }
+
+ let residentSizeKb: Double?
+ if taskVmInfoCountCovers(\task_vm_info_data_t.resident_size, count: count) {
+ residentSizeKb = Double(info.resident_size) / 1024.0
+ } else {
+ residentSizeKb = nil
+ }
+
+ let regionCount: Double?
+ if taskVmInfoCountCovers(\task_vm_info_data_t.region_count, count: count) {
+ regionCount = Double(info.region_count)
+ } else {
+ regionCount = nil
}
- return Double(info.phys_footprint) / 1_048_576.0
+ return TaskVmInfoSample(
+ physFootprintMb: physFootprintMb,
+ residentSizeKb: residentSizeKb,
+ regionCount: regionCount
+ )
}
func getDeviceMaxRefreshRate() throws -> Double {
diff --git a/src/hooks/jsThreadHooks.ts b/src/hooks/jsThreadHooks.ts
index 453d11c..5716ee1 100644
--- a/src/hooks/jsThreadHooks.ts
+++ b/src/hooks/jsThreadHooks.ts
@@ -13,6 +13,41 @@ const getValueFromBuffer = (buffer: ArrayBuffer) => {
return view.getInt32(0, true) // true = littleEndian
}
+export type ExtendedMemoryUsage = {
+ /** Primary memory usage in MB: phys_footprint on iOS and PSS on Android. */
+ memoryUsageMb: number
+ /**
+ * Resident set size in KB. Populated on iOS from `task_vm_info.resident_size`.
+ * Returns `0` on platforms that only expose the 4-byte primary memory buffer.
+ */
+ residentSizeKb: number
+ /**
+ * VM region count. Populated on iOS from `task_vm_info.region_count`.
+ * Returns `0` on platforms that only expose the 4-byte primary memory buffer.
+ */
+ regionCount: number
+}
+
+/**
+ * Read the extended iOS memory buffer layout:
+ * offset 0 : int32 phys_footprint (MB)
+ * offset 8 : float64 resident_size (KB)
+ * offset 16 : float64 region_count (count)
+ *
+ * On Android (and older library versions) the buffer is only 4 bytes, so the extra
+ * fields are returned as `0`. `getMemoryUsage()` remains the primary API and
+ * continues to return the Int32 at offset 0.
+ */
+export const getExtendedMemoryUsage = (): ExtendedMemoryUsage => {
+ const buffer = getMemoryUsageBuffer()
+ const view = new DataView(buffer)
+ return {
+ memoryUsageMb: view.byteLength >= 4 ? view.getInt32(0, true) : 0,
+ residentSizeKb: view.byteLength >= 16 ? view.getFloat64(8, true) : 0,
+ regionCount: view.byteLength >= 24 ? view.getFloat64(16, true) : 0,
+ }
+}
+
export const getJsFps = () => getValueFromBuffer(getJsFpsBuffer())
export const getUiFps = () => getValueFromBuffer(getUiFpsBuffer())
export const getCpuUsage = () => getValueFromBuffer(getCpuUsageBuffer())
@@ -43,6 +78,19 @@ export const onFpsUiChange = prepareOnChange(getUiFpsBuffer)
export const onCpuChange = prepareOnChange(getCpuUsageBuffer)
export const onMemoryChange = prepareOnChange(getMemoryUsageBuffer)
+export const onExtendedMemoryChange = (
+ callback: (value: ExtendedMemoryUsage) => void,
+ intervalMs: number = 1000
+) => {
+ const intervalId = setInterval(() => {
+ callback(getExtendedMemoryUsage())
+ }, intervalMs)
+
+ return () => {
+ clearInterval(intervalId)
+ }
+}
+
export const useFpsJs = () => {
const [value, setValue] = useState(0)
useEffect(() => {
@@ -78,3 +126,16 @@ export const useMemoryUsage = () => {
}, [])
return value
}
+
+export const useExtendedMemoryUsage = (): ExtendedMemoryUsage => {
+ const [value, setValue] = useState({
+ memoryUsageMb: 0,
+ residentSizeKb: 0,
+ regionCount: 0,
+ })
+ useEffect(() => {
+ const unsubscribe = onExtendedMemoryChange(setValue)
+ return unsubscribe
+ }, [])
+ return value
+}