diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a26c1553..9d024ca890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Add memory usage to contexts ([#2133](https://github.com/getsentry/sentry-dart/pull/2133)) + - Only for Linux/Windows applications, as iOS/Android/macOS use native SDKs + ### Fixes - App starts hanging for 30s ([#2140](https://github.com/getsentry/sentry-dart/pull/2140)) diff --git a/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart b/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart index 52243a8572..63304cdbf6 100644 --- a/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart +++ b/dart/lib/src/event_processor/enricher/io_enricher_event_processor.dart @@ -2,6 +2,7 @@ import 'dart:io'; import '../../../sentry.dart'; import 'enricher_event_processor.dart'; +import 'io_platform_memory.dart'; EnricherEventProcessor enricherEventProcessor(SentryOptions options) { return IoEnricherEventProcessor(options); @@ -17,25 +18,29 @@ class IoEnricherEventProcessor implements EnricherEventProcessor { @override SentryEvent? apply(SentryEvent event, Hint hint) { + // Amend app with current memory usage, as this is not available on native. + final app = _getApp(event.contexts.app); + // If there's a native integration available, it probably has better // information available than Flutter. - final os = _options.platformChecker.hasNativeIntegration - ? null - : _getOperatingSystem(event.contexts.operatingSystem); - final device = _options.platformChecker.hasNativeIntegration ? null : _getDevice(event.contexts.device); + final os = _options.platformChecker.hasNativeIntegration + ? null + : _getOperatingSystem(event.contexts.operatingSystem); + final culture = _options.platformChecker.hasNativeIntegration ? null : _getSentryCulture(event.contexts.culture); final contexts = event.contexts.copyWith( - operatingSystem: os, device: device, + operatingSystem: os, runtimes: _getRuntimes(event.contexts.runtimes), + app: app, culture: culture, ); @@ -97,9 +102,18 @@ class IoEnricherEventProcessor implements EnricherEventProcessor { } SentryDevice _getDevice(SentryDevice? device) { + final platformMemory = PlatformMemory(_options); return (device ?? SentryDevice()).copyWith( name: device?.name ?? Platform.localHostname, processorCount: device?.processorCount ?? Platform.numberOfProcessors, + memorySize: device?.memorySize ?? platformMemory.getTotalPhysicalMemory(), + freeMemory: device?.freeMemory ?? platformMemory.getFreePhysicalMemory(), + ); + } + + SentryApp _getApp(SentryApp? app) { + return (app ?? SentryApp()).copyWith( + appMemory: app?.appMemory ?? ProcessInfo.currentRss, ); } diff --git a/dart/lib/src/event_processor/enricher/io_platform_memory.dart b/dart/lib/src/event_processor/enricher/io_platform_memory.dart new file mode 100644 index 0000000000..4acde3f15b --- /dev/null +++ b/dart/lib/src/event_processor/enricher/io_platform_memory.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import '../../protocol.dart'; +import '../../sentry_options.dart'; + +// Get total & free platform memory (in bytes) for linux and windows operating systems. +// Source: https://github.com/onepub-dev/system_info/blob/8a9bf6b8eb7c86a09b3c3df4bf6d7fa5a6b50732/lib/src/platform/memory.dart +class PlatformMemory { + PlatformMemory(this.options); + + final SentryOptions options; + + int? getTotalPhysicalMemory() { + if (options.platformChecker.platform.isLinux) { + return _getLinuxMemInfoValue('MemTotal'); + } else if (options.platformChecker.platform.isWindows) { + return _getWindowsWmicValue('ComputerSystem', 'TotalPhysicalMemory'); + } else { + return null; + } + } + + int? getFreePhysicalMemory() { + if (options.platformChecker.platform.isLinux) { + return _getLinuxMemInfoValue('MemFree'); + } else if (options.platformChecker.platform.isWindows) { + return _getWindowsWmicValue('OS', 'FreePhysicalMemory'); + } else { + return null; + } + } + + int? _getWindowsWmicValue(String section, String key) { + final os = _wmicGetValueAsMap(section, [key]); + final totalPhysicalMemoryValue = os?[key]; + if (totalPhysicalMemoryValue == null) { + return null; + } + final size = int.tryParse(totalPhysicalMemoryValue); + if (size == null) { + return null; + } + return size; + } + + int? _getLinuxMemInfoValue(String key) { + final meminfoList = _exec('cat', ['/proc/meminfo']) + ?.trim() + .replaceAll('\r\n', '\n') + .split('\n') ?? + []; + + final meminfoMap = _listToMap(meminfoList, ':'); + final memsizeResults = meminfoMap[key]?.split(' ') ?? []; + + if (memsizeResults.isEmpty) { + return null; + } + final memsizeResult = memsizeResults.first; + + final memsize = int.tryParse(memsizeResult); + if (memsize == null) { + return null; + } + return memsize; + } + + String? _exec(String executable, List arguments, + {bool runInShell = false}) { + try { + final result = + Process.runSync(executable, arguments, runInShell: runInShell); + if (result.exitCode == 0) { + return result.stdout.toString(); + } + } catch (e) { + options.logger(SentryLevel.warning, "Failed to run process: $e"); + } + return null; + } + + Map? _wmicGetValueAsMap(String section, List fields) { + final arguments = [section]; + arguments + ..add('get') + ..addAll(fields.join(', ').split(' ')) + ..add('/VALUE'); + + final list = + _exec('wmic', arguments)?.trim().replaceAll('\r\n', '\n').split('\n') ?? + []; + + return _listToMap(list, '='); + } + + Map _listToMap(List list, String separator) { + final map = {}; + for (final string in list) { + final index = string.indexOf(separator); + if (index != -1) { + final key = string.substring(0, index).trim(); + final value = string.substring(index + 1).trim(); + map[key] = value; + } + } + return map; + } +} diff --git a/dart/test/event_processor/enricher/io_platform_memory_test.dart b/dart/test/event_processor/enricher/io_platform_memory_test.dart new file mode 100644 index 0000000000..0f987b935c --- /dev/null +++ b/dart/test/event_processor/enricher/io_platform_memory_test.dart @@ -0,0 +1,60 @@ +@TestOn('vm') +library dart_test; + +import 'dart:io'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/event_processor/enricher/io_platform_memory.dart'; +import 'package:test/test.dart'; + +void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + test('total physical memory', () { + final sut = fixture.getSut(); + final totalPhysicalMemory = sut.getTotalPhysicalMemory(); + + switch (Platform.operatingSystem) { + case 'linux': + expect(totalPhysicalMemory, isNotNull); + expect(totalPhysicalMemory! > 0, true); + break; + case 'windows': + expect(totalPhysicalMemory, isNotNull); + expect(totalPhysicalMemory! > 0, true); + break; + default: + expect(totalPhysicalMemory, isNull); + } + }); + + test('free physical memory', () { + final sut = fixture.getSut(); + final freePhysicalMemory = sut.getTotalPhysicalMemory(); + + switch (Platform.operatingSystem) { + case 'linux': + expect(freePhysicalMemory, isNotNull); + expect(freePhysicalMemory! > 0, true); + break; + case 'windows': + expect(freePhysicalMemory, isNotNull); + expect(freePhysicalMemory! > 0, true); + break; + default: + expect(freePhysicalMemory, isNull); + } + }); +} + +class Fixture { + var options = SentryOptions(); + + PlatformMemory getSut() { + return PlatformMemory(options); + } +}