1
+// code shamelessly stolen from xmruw
2
+// https://raw.githubusercontent.com/MrCyjaneK/unnamed_monero_wallet/refs/heads/master-rewrite/lib/pages/debug/performance.dart
3
+import 'dart:math';
4
+
5
+import 'package:cake_wallet/di.dart';
6
+import 'package:cake_wallet/monero/monero.dart';
7
+import 'package:cake_wallet/src/widgets/primary_button.dart';
8
+import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
9
+import 'package:cake_wallet/wownero/wownero.dart';
10
+import 'package:cake_wallet/zano/zano.dart';
11
+import 'package:cw_core/wallet_type.dart';
12
+import 'package:flutter/material.dart';
13
+import 'package:cake_wallet/src/screens/base_page.dart';
14
+
15
+class DevMoneroCallProfilerPage extends BasePage {
16
+ DevMoneroCallProfilerPage();
17
+
18
+ @override
19
+ String? get title => "[dev] xmr call profiler";
20
+
21
+ @override
22
+ Widget body(BuildContext context) {
23
+ return PerformanceDebug();
24
+ }
25
+}
26
+
27
+
28
+
29
+class PerformanceDebug extends StatefulWidget {
30
+ const PerformanceDebug({super.key});
31
+
32
+ @override
33
+ State<PerformanceDebug> createState() => _PerformanceDebugState();
34
+}
35
+
36
+enum ProfilableWallet {
37
+ monero,
38
+ wownero,
39
+ zano,
40
+}
41
+
42
+class _PerformanceDebugState extends State<PerformanceDebug> {
43
+ List<Widget> widgets = [];
44
+
45
+ final dashboardViewModel = getIt.get<DashboardViewModel>();
46
+
47
+ late ProfilableWallet wallet = switch (dashboardViewModel.wallet.type) {
48
+ WalletType.monero => ProfilableWallet.monero,
49
+ WalletType.wownero => ProfilableWallet.wownero,
50
+ WalletType.zano => ProfilableWallet.zano,
51
+ _ => throw Exception("Unknown wallet type"),
52
+ };
53
+ final precalc = 1700298;
54
+
55
+ late Map<String, List<int>> debugCallLength = switch (wallet) {
56
+ ProfilableWallet.monero => monero!.debugCallLength(),
57
+ ProfilableWallet.wownero => wownero!.debugCallLength(),
58
+ ProfilableWallet.zano => zano!.debugCallLength(),
59
+ };
60
+
61
+ int getOpenWalletTime() {
62
+ if (debugCallLength["MONERO_Wallet_init"] == null) {
63
+ return precalc;
64
+ }
65
+ if (debugCallLength["MONERO_Wallet_init"]!.isEmpty) {
66
+ return precalc;
67
+ }
68
+ return debugCallLength["MONERO_Wallet_init"]!.last;
69
+ }
70
+
71
+late final String perfInfo = """
72
+---- Performance tuning
73
+This page lists all calls that take place during the app runtime.-
74
+As per Flutter docs we can read:
75
+> Flutter aims to provide 60 frames per second (fps) performance, or 120 fps-
76
+performance on devices capable of 120Hz updates.
77
+
78
+With that in mind we will aim to render frames every 8.3ms (~8333 µs). It is-
79
+however acceptable to reach 16.6 ms (~16666 µs) but we should also keep in mind-
80
+that there are also UI costs that aren't part of this benchmark.
81
+
82
+For some calls it is also acceptable to exceed this amount of time, for example-
83
+MONERO_Wallet_init takes ~${getOpenWalletTime()}µs-
84
+(${(getOpenWalletTime() / frameTime).toStringAsFixed(2)} frames). That time would-
85
+be unnaceptable in most situations but since we call this function only when-
86
+opening the wallet it is completely fine to freeze the UI for the time being --
87
+as the user won't even notice that something happened.
88
+
89
+---- Details
90
+count: how many times did we call this function [total time (% of frame)]
91
+average: average execution time (% of frame)
92
+min: fastest execution (% of frame)
93
+max: slowest execution (% of frame)
94
+95th: 95% of the time, the function is faster than this amount of time (% of frame)
95
+"""
96
+ .split("-\n")
97
+ .join(" ");
98
+
99
+ late final frameTime = 8333;
100
+ late final frameGreenTier = frameTime ~/ 100;
101
+ late final frameBlueTier = frameTime ~/ 10;
102
+ late final frameBlueGreyTier = frameTime ~/ 2;
103
+ late final frameYellowTier = frameTime;
104
+ late final frameOrangeTier = frameTime * 2;
105
+
106
+ Color? perfc(num frame) {
107
+ if (frame < frameGreenTier) return Colors.green;
108
+ if (frame < frameBlueTier) return Colors.blue;
109
+ if (frame < frameBlueGreyTier) return Colors.blueGrey;
110
+ if (frame < frameGreenTier) return Colors.green;
111
+ if (frame < frameYellowTier) return Colors.yellow;
112
+ if (frame < frameOrangeTier) return Colors.orange;
113
+ return Colors.red;
114
+ }
115
+
116
+
117
+ @override
118
+ void initState() {
119
+ _buildWidgets();
120
+ super.initState();
121
+ }
122
+
123
+ SelectableText cw(String text, Color? color) {
124
+ return SelectableText(
125
+ text,
126
+ style: TextStyle(color: color),
127
+ );
128
+ }
129
+
130
+ void _buildWidgets() {
131
+ List<Widget> ws = [];
132
+ ws.add(Column(
133
+ mainAxisSize: MainAxisSize.min,
134
+ mainAxisAlignment: MainAxisAlignment.start,
135
+ crossAxisAlignment: CrossAxisAlignment.start,
136
+ children: [
137
+ SelectableText(perfInfo),
138
+ cw("< 1% of a frame (max: $frameGreenTierµs)", Colors.green),
139
+ cw("< 10% of a frame (max: $frameBlueTierµs)", Colors.blue),
140
+ cw("< 50% of a frame (max: $frameBlueGreyTierµs)", Colors.blueGrey),
141
+ cw("< 100% of a frame (max: $frameYellowTierµs)", Colors.yellow),
142
+ cw("< 200% of a frame (max: $frameOrangeTierµs)", Colors.orange),
143
+ cw("> 200% of a frame (UI junk visible)", Colors.red),
144
+ ],
145
+ ));
146
+ final keys = debugCallLength.keys.toList();
147
+ keys.sort((s1, s2) =>
148
+ _n95th(debugCallLength[s2]!) -
149
+ _n95th(debugCallLength[s1]!));
150
+ for (var key in keys) {
151
+ final value = debugCallLength[key];
152
+ if (value == null) continue;
153
+ final avg = _avg(value);
154
+ final min = _min(value);
155
+ final max = _max(value);
156
+ final np = _n95th(value);
157
+ final total = _total(value);
158
+ ws.add(
159
+ Card(
160
+ child: ListTile(
161
+ title: Text(
162
+ key,
163
+ style: TextStyle(color: perfc(np)),
164
+ ),
165
+ subtitle: Column(
166
+ mainAxisSize: MainAxisSize.min,
167
+ mainAxisAlignment: MainAxisAlignment.start,
168
+ crossAxisAlignment: CrossAxisAlignment.start,
169
+ children: [
170
+ Row(children: [
171
+ cw("count: ${value.length}", null),
172
+ const Spacer(),
173
+ cw("${_str(total / 1000)}ms", perfc(total)),
174
+ ]),
175
+ cw("average: ${_str(avg)}µs (~${_str(avg / (frameTime))}f)",
176
+ perfc(avg)),
177
+ cw("min: $minµs (~${_str(min / (frameTime) * 100)})",
178
+ perfc(min)),
179
+ cw("max: $maxµs (~${_str(max / (frameTime) * 100)}%)",
180
+ perfc(max)),
181
+ cw("95th: $npµs (~${_str(np / (frameTime) * 100)}%)",
182
+ perfc(np)),
183
+ ],
184
+ ),
185
+ ),
186
+ ),
187
+ );
188
+ }
189
+ if (debugCallLength.isNotEmpty) {
190
+ ws.add(
191
+ PrimaryButton(
192
+ text: "Purge statistics",
193
+ onPressed: _purgeStats,
194
+ color: Colors.red,
195
+ textColor: Colors.white,
196
+ ),
197
+ );
198
+ }
199
+ setState(() {
200
+ widgets = ws;
201
+ });
202
+ }
203
+
204
+ void _purgeStats() {
205
+ debugCallLength.clear();
206
+ _buildWidgets();
207
+ }
208
+
209
+ int _min(List<int> l) {
210
+ return l.reduce(min);
211
+ }
212
+
213
+ int _max(List<int> l) {
214
+ return l.reduce(max);
215
+ }
216
+
217
+ int _n95th(List<int> l) {
218
+ final l0 = l.toList();
219
+ l0.sort();
220
+ int i = (0.95 * l.length).ceil() - 1;
221
+ return l0[i];
222
+ }
223
+
224
+ double _avg(List<int> l) {
225
+ int c = 0;
226
+ for (var i = 0; i < l.length; i++) {
227
+ c += l[i];
228
+ }
229
+ return c / l.length;
230
+ }
231
+
232
+ int _total(List<int> l) {
233
+ int c = 0;
234
+ for (var i = 0; i < l.length; i++) {
235
+ c += l[i];
236
+ }
237
+ return c;
238
+ }
239
+
240
+ String _str(num d) => d.toStringAsFixed(2);
241
+
242
+ @override
243
+ Widget build(BuildContext context) {
244
+ return SingleChildScrollView(
245
+ child: Padding(
246
+ padding: const EdgeInsets.all(8),
247
+ child: Column(
248
+ children: widgets,
249
+ ),
250
+ ),
251
+ );
252
+ }
253
+}