Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
Public Member Functions | Public Attributes | Protected Member Functions | Protected Attributes | List of all members
flavor.android.AndroidFlavor Class Reference
Inheritance diagram for flavor.android.AndroidFlavor:
flavor.default.DefaultFlavor

Public Member Functions

 __init__ (self, m, app_name)
 
 install (self)
 
 cleanup_steps (self)
 
 step (self, name, cmd)
 
 copy_file_to_device (self, host, device)
 
 copy_directory_contents_to_device (self, host, device)
 
 copy_directory_contents_to_host (self, device, host)
 
 read_file_on_device (self, path, **kwargs)
 
 remove_file_on_device (self, path)
 
 create_clean_device_dir (self, path)
 
- Public Member Functions inherited from flavor.default.DefaultFlavor
 device_path_join (self, *args)
 
 create_clean_host_dir (self, path)
 

Public Attributes

 ADB_BINARY
 
 ADB_PUB_KEY
 
 device_dirs
 
 cant_root
 
 cpus_to_scale
 
 disable_for_nanobench
 
 gpu_scaling
 
 app_name
 
- Public Attributes inherited from flavor.default.DefaultFlavor
 app_name
 
 module
 
 m
 
 device_dirs
 
 host_dirs
 

Protected Member Functions

 _wait_for_device (self, title, attempt)
 
 _adb (self, title, *cmd, **kwargs)
 
 _scale_for_dm (self)
 
 _scale_for_nanobench (self)
 
 _set_governor (self, cpu, gov)
 
 _set_cpu_online (self, cpu, value)
 
 _scale_cpu (self, cpu, target_percent)
 
 _asan_setup_path (self)
 
- Protected Member Functions inherited from flavor.default.DefaultFlavor
 _run (self, title, cmd, infra_step=False, **kwargs)
 
 _py (self, title, script, infra_step=True, args=())
 

Protected Attributes

 _ever_ran_adb
 
- Protected Attributes inherited from flavor.default.DefaultFlavor
 _chrome_path
 

Detailed Description

Definition at line 16 of file android.py.

Constructor & Destructor Documentation

◆ __init__()

flavor.android.AndroidFlavor.__init__ (   self,
  m,
  app_name 
)

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 17 of file android.py.

17 def __init__(self, m, app_name):
18 super(AndroidFlavor, self).__init__(m, app_name)
19 self._ever_ran_adb = False
20 self.ADB_BINARY = '/usr/bin/adb.1.0.35'
21 self.ADB_PUB_KEY = '/home/chrome-bot/.android/adbkey'
22 if 'skia' not in self.m.vars.swarming_bot_id:
23 self.ADB_BINARY = '/opt/infra-android/tools/adb'
24 self.ADB_PUB_KEY = ('/home/chrome-bot/.android/'
25 'chrome_infrastructure_adbkey')
26
27 # Data should go in android_data_dir, which may be preserved across runs.
28 android_data_dir = '/sdcard/revenge_of_the_skiabot/'
29 self.device_dirs = default.DeviceDirs(
30 bin_dir = '/data/local/tmp/',
31 dm_dir = android_data_dir + 'dm_out',
32 perf_data_dir = android_data_dir + 'perf',
33 resource_dir = android_data_dir + 'resources',
34 fonts_dir = 'NOT_SUPPORTED',
35 images_dir = android_data_dir + 'images',
36 lotties_dir = android_data_dir + 'lotties',
37 skp_dir = android_data_dir + 'skps',
38 svg_dir = android_data_dir + 'svgs',
39 mskp_dir = android_data_dir + 'mskp',
40 tmp_dir = android_data_dir,
41 texttraces_dir = android_data_dir + 'text_blob_traces')
42
43 # A list of devices we can't root. If rooting fails and a device is not
44 # on the list, we fail the task to avoid perf inconsistencies.
45 self.cant_root = ['GalaxyS7_G930FD', 'GalaxyS9',
46 'GalaxyS20', 'MotoG4', 'NVIDIA_Shield',
47 'P30', 'Pixel4','Pixel4XL', 'Pixel5', 'TecnoSpark3Pro', 'JioNext']
48
49 # Maps device type -> CPU ids that should be scaled for nanobench.
50 # Many devices have two (or more) different CPUs (e.g. big.LITTLE
51 # on Nexus5x). The CPUs listed are the biggest cpus on the device.
52 # The CPUs are grouped together, so we only need to scale one of them
53 # (the one listed) in order to scale them all.
54 # E.g. Nexus5x has cpu0-3 as one chip and cpu4-5 as the other. Thus,
55 # if one wants to run a single-threaded application (e.g. nanobench), one
56 # can disable cpu0-3 and scale cpu 4 to have only cpu4 and 5 at the same
57 # frequency. See also disable_for_nanobench.
58 self.cpus_to_scale = {
59 'Nexus5x': [4],
60 'Pixel': [2],
61 'Pixel2XL': [4]
62 }
63
64 # Maps device type -> CPU ids that should be turned off when running
65 # single-threaded applications like nanobench. The devices listed have
66 # multiple, differnt CPUs. We notice a lot of noise that seems to be
67 # caused by nanobench running on the slow CPU, then the big CPU. By
68 # disabling this, we see less of that noise by forcing the same CPU
69 # to be used for the performance testing every time.
70 self.disable_for_nanobench = {
71 'Nexus5x': range(0, 4),
72 'Pixel': range(0, 2),
73 'Pixel2XL': range(0, 4),
74 'Pixel6': range(4,8), # Only use the 4 small cores.
75 'Pixel7': range(4,8),
76 }
77
78 self.gpu_scaling = {
79 "Nexus5": 450000000,
80 "Nexus5x": 600000000,
81 }
82

Member Function Documentation

◆ _adb()

flavor.android.AndroidFlavor._adb (   self,
  title,
cmd,
**  kwargs 
)
protected

Definition at line 130 of file android.py.

130 def _adb(self, title, *cmd, **kwargs):
131 # The only non-infra adb steps (dm / nanobench) happen to not use _adb().
132 if 'infra_step' not in kwargs:
133 kwargs['infra_step'] = True
134
135 self._ever_ran_adb = True
136 # ADB seems to be occasionally flaky on every device, so always retry.
137 attempts = kwargs.pop('attempts', 3)
138
139 def wait_for_device(attempt):
140 return self._wait_for_device(title, attempt)
141
142 with self.m.context(cwd=self.m.path['start_dir'].join('skia')):
143 with self.m.env({'ADB_VENDOR_KEYS': self.ADB_PUB_KEY}):
144 return self.m.run.with_retry(self.m.step, title, attempts,
145 cmd=[self.ADB_BINARY]+list(cmd),
146 between_attempts_fn=wait_for_device,
147 **kwargs)
148
Definition __init__.py:1

◆ _asan_setup_path()

flavor.android.AndroidFlavor._asan_setup_path (   self)
protected

Definition at line 255 of file android.py.

255 def _asan_setup_path(self):
256 return self.m.vars.workdir.join(
257 'android_ndk_linux', 'toolchains', 'llvm', 'prebuilt', 'linux-x86_64',
258 'lib', 'clang', '17', 'bin', 'asan_device_setup')
259
260

◆ _scale_cpu()

flavor.android.AndroidFlavor._scale_cpu (   self,
  cpu,
  target_percent 
)
protected

Definition at line 239 of file android.py.

239 def _scale_cpu(self, cpu, target_percent):
240 self._ever_ran_adb = True
241
242 def wait_for_device(attempt):
243 return self._wait_for_device("scale cpu", attempt)
244
245 script = self.module.resource('scale_cpu.py')
246 self.m.run.with_retry(self.m.step,
247 'Scale CPU %d to %f' % (cpu, target_percent),
248 3, # attempts
249 cmd=['python3', script, self.ADB_BINARY, str(target_percent), cpu],
250 infra_step=True,
251 between_attempts_fn=wait_for_device,
252 timeout=30)
253
254

◆ _scale_for_dm()

flavor.android.AndroidFlavor._scale_for_dm (   self)
protected

Definition at line 149 of file android.py.

149 def _scale_for_dm(self):
150 device = self.m.vars.builder_cfg.get('model')
151 if (device in self.cant_root or
152 self.m.vars.internal_hardware_label):
153 return
154
155 # This is paranoia... any CPUs we disabled while running nanobench
156 # ought to be back online now that we've restarted the device.
157 for i in self.disable_for_nanobench.get(device, []):
158 self._set_cpu_online(i, 1) # enable
159
160 scale_up = self.cpus_to_scale.get(device, [0])
161 # For big.LITTLE devices, make sure we scale the LITTLE cores up;
162 # there is a chance they are still in powersave mode from when
163 # swarming slows things down for cooling down and charging.
164 if 0 not in scale_up:
165 scale_up.append(0)
166 for i in scale_up:
167 # AndroidOne doesn't support ondemand governor. hotplug is similar.
168 if device == 'AndroidOne':
169 self._set_governor(i, 'hotplug')
170 elif device in ['Pixel3a', 'Pixel4', 'Pixel4a', 'Wembley', 'Pixel6', 'Pixel7']:
171 # Pixel3a/4/4a have userspace powersave performance schedutil.
172 # performance seems like a reasonable choice.
173 self._set_governor(i, 'performance')
174 else:
175 self._set_governor(i, 'ondemand')
176

◆ _scale_for_nanobench()

flavor.android.AndroidFlavor._scale_for_nanobench (   self)
protected

Definition at line 177 of file android.py.

177 def _scale_for_nanobench(self):
178 device = self.m.vars.builder_cfg.get('model')
179 if (device in self.cant_root or
180 self.m.vars.internal_hardware_label):
181 return
182
183 # Set to 'powersave' for Pixel6 and Pixel7.
184 for i in self.cpus_to_scale.get(device, [0]):
185 if device in ['Pixel6', 'Pixel7']:
186 self._set_governor(i, 'powersave')
187 else:
188 self._set_governor(i, 'userspace')
189 self._scale_cpu(i, 0.6)
190
191 for i in self.disable_for_nanobench.get(device, []):
192 self._set_cpu_online(i, 0) # disable
193
194 if device in self.gpu_scaling:
195 #https://developer.qualcomm.com/qfile/28823/lm80-p0436-11_adb_commands.pdf
196 # Section 3.2.1 Commands to put the GPU in performance mode
197 # Nexus 5 is 320000000 by default
198 # Nexus 5x is 180000000 by default
199 gpu_freq = self.gpu_scaling[device]
200 script = self.module.resource('set_gpu_scaling.py')
201 self.m.run.with_retry(self.m.step,
202 "Lock GPU to %d (and other perf tweaks)" % gpu_freq,
203 3, # attempts
204 cmd=['python3', script, self.ADB_BINARY, gpu_freq],
205 infra_step=True,
206 timeout=30)
207

◆ _set_cpu_online()

flavor.android.AndroidFlavor._set_cpu_online (   self,
  cpu,
  value 
)
protected
Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1).

Definition at line 219 of file android.py.

219 def _set_cpu_online(self, cpu, value):
220 """Set /sys/devices/system/cpu/cpu{N}/online to value (0 or 1)."""
221 self._ever_ran_adb = True
222 msg = 'Disabling'
223 if value:
224 msg = 'Enabling'
225
226 def wait_for_device(attempt):
227 return self._wait_for_device("set cpu online", attempt) # pragma: nocover
228
229 script = self.module.resource('set_cpu_online.py')
230 self.m.run.with_retry(self.m.step,
231 '%s CPU %d' % (msg, cpu),
232 3, # attempts
233 cmd=['python3', script, self.ADB_BINARY, cpu, value],
234 infra_step=True,
235 between_attempts_fn=wait_for_device,
236 timeout=30)
237
238

◆ _set_governor()

flavor.android.AndroidFlavor._set_governor (   self,
  cpu,
  gov 
)
protected

Definition at line 208 of file android.py.

208 def _set_governor(self, cpu, gov):
209 self._ever_ran_adb = True
210 script = self.module.resource('set_cpu_scaling_governor.py')
211 self.m.run.with_retry(self.m.step,
212 "Set CPU %d's governor to %s" % (cpu, gov),
213 3, # attempts
214 cmd=['python3', script, self.ADB_BINARY, cpu, gov],
215 infra_step=True,
216 timeout=30)
217
218

◆ _wait_for_device()

flavor.android.AndroidFlavor._wait_for_device (   self,
  title,
  attempt 
)
protected

Definition at line 83 of file android.py.

83 def _wait_for_device(self, title, attempt):
84 self.m.run(self.m.step,
85 'adb kill-server after failure of \'%s\' (attempt %d)' % (
86 title, attempt),
87 cmd=[self.ADB_BINARY, 'kill-server'],
88 infra_step=True, timeout=30, abort_on_failure=False,
89 fail_build_on_failure=False)
90 self.m.run(self.m.step,
91 'wait for device after failure of \'%s\' (attempt %d)' % (
92 title, attempt),
93 cmd=[self.ADB_BINARY, 'wait-for-device'], infra_step=True,
94 timeout=180, abort_on_failure=False,
95 fail_build_on_failure=False)
96 self.m.run(self.m.step,
97 'adb devices -l after failure of \'%s\' (attempt %d)' % (
98 title, attempt),
99 cmd=[self.ADB_BINARY, 'devices', '-l'],
100 infra_step=True, timeout=30, abort_on_failure=False,
101 fail_build_on_failure=False)
102 self.m.run(self.m.step,
103 'adb reboot device after failure of \'%s\' (attempt %d)' % (
104 title, attempt),
105 cmd=[self.ADB_BINARY, 'reboot'],
106 infra_step=True, timeout=30, abort_on_failure=False,
107 fail_build_on_failure=False)
108 self.m.run(self.m.step,
109 'wait for device after failure of \'%s\' (attempt %d)' % (
110 title, attempt),
111 cmd=[
112 self.ADB_BINARY, 'wait-for-device', 'shell',
113 # Wait until the boot is actually complete.
114 # https://android.stackexchange.com/a/164050
115 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
116 ],
117 timeout=180, abort_on_failure=False,
118 fail_build_on_failure=False)
119 device = self.m.vars.builder_cfg.get('model')
120 if (device in self.cant_root): # pragma: nocover
121 return
122 self.m.run(self.m.step,
123 'adb root',
124 cmd=[
125 self.ADB_BINARY, 'root'
126 ],
127 timeout=180, abort_on_failure=False,
128 fail_build_on_failure=False)
129
Definition run.py:1

◆ cleanup_steps()

flavor.android.AndroidFlavor.cleanup_steps (   self)
Run any device-specific cleanup steps.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 290 of file android.py.

290 def cleanup_steps(self):
291 self.m.run(self.m.step,
292 'adb reboot device',
293 cmd=[self.ADB_BINARY, 'reboot'],
294 infra_step=True, timeout=30, abort_on_failure=False,
295 fail_build_on_failure=False)
296 self.m.run(self.m.step,
297 'wait for device after rebooting',
298 cmd=[
299 self.ADB_BINARY, 'wait-for-device', 'shell',
300 # Wait until the boot is actually complete.
301 # https://android.stackexchange.com/a/164050
302 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
303 ],
304 timeout=180, abort_on_failure=False,
305 fail_build_on_failure=False)
306
307 if 'ASAN' in self.m.vars.extra_tokens:
308 self._ever_ran_adb = True
309 # Remove ASAN.
310 self.m.run(self.m.step,
311 'wait for device before uninstalling ASAN',
312 cmd=[self.ADB_BINARY, 'wait-for-device', 'shell',
313 # Wait until the boot is actually complete.
314 # https://android.stackexchange.com/a/164050
315 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done',
316 ], infra_step=True,
317 timeout=180, abort_on_failure=False,
318 fail_build_on_failure=False)
319 self.m.run(self.m.step, 'uninstall ASAN',
320 cmd=[self._asan_setup_path(), '--revert'],
321 infra_step=True, timeout=300,
322 abort_on_failure=False, fail_build_on_failure=False)
323
324 if self._ever_ran_adb:
325 script = self.module.resource('dump_adb_log.py')
326 self.m.run(self.m.step, 'dump log',
327 cmd=['python3', script, self.host_dirs.bin_dir, self.ADB_BINARY],
328 infra_step=True,
329 timeout=300,
330 abort_on_failure=False)
331
332 # Only quarantine the bot if the first failed step
333 # is an infra step. If, instead, we did this for any infra failures, we
334 # would do this too much. For example, if a Nexus 10 died during dm
335 # and the following pull step would also fail "device not found" - causing
336 # us to run the shutdown command when the device was probably not in a
337 # broken state; it was just rebooting.
338 if (self.m.run.failed_steps and
339 isinstance(self.m.run.failed_steps[0], recipe_api.InfraFailure)):
340 bot_id = self.m.vars.swarming_bot_id
341 self.m.file.write_text('Quarantining Bot',
342 '/home/chrome-bot/%s.force_quarantine' % bot_id,
343 ' ')
344
345 # if self._ever_ran_adb:
346 # self._adb('kill adb server', 'kill-server')
347

◆ copy_directory_contents_to_device()

flavor.android.AndroidFlavor.copy_directory_contents_to_device (   self,
  host_dir,
  device_dir 
)
Like shutil.copytree(), but for copying to a connected device.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 366 of file android.py.

366 def copy_directory_contents_to_device(self, host, device):
367 contents = self.m.file.glob_paths('ls %s/*' % host,
368 host, '*',
369 test_data=['foo.png', 'bar.jpg'])
370 args = contents + [device]
371 self._adb('push %s/* %s' % (host, device), 'push', *args)
372

◆ copy_directory_contents_to_host()

flavor.android.AndroidFlavor.copy_directory_contents_to_host (   self,
  device_dir,
  host_dir 
)
Like shutil.copytree(), but for copying from a connected device.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 373 of file android.py.

373 def copy_directory_contents_to_host(self, device, host):
374 # TODO(borenet): When all of our devices are on Android 6.0 and up, we can
375 # switch to using tar to zip up the results before pulling.
376 with self.m.step.nest('adb pull'):
377 tmp = self.m.path.mkdtemp('adb_pull')
378 self._adb('pull %s' % device, 'pull', device, tmp)
379 paths = self.m.file.glob_paths(
380 'list pulled files',
381 tmp,
382 self.m.path.basename(device) + self.m.path.sep + '*',
383 test_data=['%d.png' % i for i in (1, 2)])
384 for p in paths:
385 self.m.file.copy('copy %s' % self.m.path.basename(p), p, host)
386

◆ copy_file_to_device()

flavor.android.AndroidFlavor.copy_file_to_device (   self,
  host_path,
  device_path 
)
Like shutil.copyfile, but for copying to a connected device.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 363 of file android.py.

363 def copy_file_to_device(self, host, device):
364 self._adb('push %s %s' % (host, device), 'push', host, device)
365

◆ create_clean_device_dir()

flavor.android.AndroidFlavor.create_clean_device_dir (   self,
  path 
)
Like shutil.rmtree() + os.makedirs(), but on a connected device.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 409 of file android.py.

409 def create_clean_device_dir(self, path):
410 self.remove_file_on_device(path)
411 self._adb('mkdir %s' % path, 'shell', 'mkdir', '-p', path)

◆ install()

flavor.android.AndroidFlavor.install (   self)
Run device-specific installation steps.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 261 of file android.py.

261 def install(self):
262 self._adb('mkdir ' + self.device_dirs.resource_dir,
263 'shell', 'mkdir', '-p', self.device_dirs.resource_dir)
264 if self.m.vars.builder_cfg.get('model') in ['GalaxyS20', 'GalaxyS9']:
265 # See skia:10184, should be moot once upgraded to Android 11?
266 self._adb('cp libGLES_mali.so to ' + self.device_dirs.bin_dir,
267 'shell', 'cp',
268 '/vendor/lib64/egl/libGLES_mali.so',
269 self.device_dirs.bin_dir + 'libvulkan.so')
270 if 'ASAN' in self.m.vars.extra_tokens:
271 self._ever_ran_adb = True
272 script = self.module.resource('setup_device_for_asan.py')
273 self.m.run(
274 self.m.step, 'Setting up device to run ASAN',
275 cmd=['python3', script, self.ADB_BINARY, self._asan_setup_path()],
276 infra_step=True,
277 timeout=300,
278 abort_on_failure=True)
279 if self.app_name:
280 if (self.app_name == 'nanobench'):
281 self._scale_for_nanobench()
282 else:
283 self._scale_for_dm()
284 app_path = self.host_dirs.bin_dir.join(self.app_name)
285 self._adb('push %s' % self.app_name,
286 'push', app_path, self.device_dirs.bin_dir)
287
288
289
static bool install(SkBitmap *bm, const SkImageInfo &info, const SkRasterHandleAllocator::Rec &rec)

◆ read_file_on_device()

flavor.android.AndroidFlavor.read_file_on_device (   self,
  path,
**  kwargs 
)
Reads the specified file.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 387 of file android.py.

387 def read_file_on_device(self, path, **kwargs):
388 testKwargs = {
389 'attempts': 1,
390 'abort_on_failure': False,
391 'fail_build_on_failure': False,
392 }
393 rv = self._adb('check if %s exists' % path,
394 'shell', 'test', '-f', path, **testKwargs)
395 if not rv: # pragma: nocover
396 return None
397
398 rv = self._adb('read %s' % path,
399 'shell', 'cat', path, stdout=self.m.raw_io.output(),
400 **kwargs)
401 return rv.stdout.decode('utf-8').rstrip() if rv and rv.stdout else None
402

◆ remove_file_on_device()

flavor.android.AndroidFlavor.remove_file_on_device (   self,
  path 
)
Removes the specified file.

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 403 of file android.py.

403 def remove_file_on_device(self, path):
404 script = self.module.resource('remove_file_on_device.py')
405 self.m.run.with_retry(self.m.step, 'rm %s' % path, 3,
406 cmd=['python3', script, self.ADB_BINARY, path],
407 infra_step=True)
408

◆ step()

flavor.android.AndroidFlavor.step (   self,
  name,
  cmd 
)

Reimplemented from flavor.default.DefaultFlavor.

Definition at line 348 of file android.py.

348 def step(self, name, cmd):
349 sh = '%s.sh' % cmd[0]
350 self.m.run.writefile(self.m.vars.tmp_dir.join(sh),
351 'set -x; LD_LIBRARY_PATH=%s %s%s; echo $? >%src' % (
352 self.device_dirs.bin_dir,
353 self.device_dirs.bin_dir, subprocess.list2cmdline(map(str, cmd)),
354 self.device_dirs.bin_dir))
355 self._adb('push %s' % sh,
356 'push', self.m.vars.tmp_dir.join(sh), self.device_dirs.bin_dir)
357
358 self._adb('clear log', 'logcat', '-c')
359 script = self.module.resource('run_sh.py')
360 self.m.step('%s' % cmd[0],
361 cmd=['python3', script, self.device_dirs.bin_dir, sh, self.ADB_BINARY])
362
static int step(int x, SkScalar min, SkScalar max)
Definition BlurTest.cpp:215

Member Data Documentation

◆ _ever_ran_adb

flavor.android.AndroidFlavor._ever_ran_adb
protected

Definition at line 19 of file android.py.

◆ ADB_BINARY

flavor.android.AndroidFlavor.ADB_BINARY

Definition at line 20 of file android.py.

◆ ADB_PUB_KEY

flavor.android.AndroidFlavor.ADB_PUB_KEY

Definition at line 21 of file android.py.

◆ app_name

flavor.android.AndroidFlavor.app_name

Definition at line 280 of file android.py.

◆ cant_root

flavor.android.AndroidFlavor.cant_root

Definition at line 45 of file android.py.

◆ cpus_to_scale

flavor.android.AndroidFlavor.cpus_to_scale

Definition at line 58 of file android.py.

◆ device_dirs

flavor.android.AndroidFlavor.device_dirs

Definition at line 29 of file android.py.

◆ disable_for_nanobench

flavor.android.AndroidFlavor.disable_for_nanobench

Definition at line 70 of file android.py.

◆ gpu_scaling

flavor.android.AndroidFlavor.gpu_scaling

Definition at line 78 of file android.py.


The documentation for this class was generated from the following file: