VirtualBox

source: vbox/trunk/src/VBox/Main/src-server/linux/PerformanceLinux.cpp

Last change on this file was 106061, checked in by vboxsync, 3 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 20.0 KB
Line 
1/* $Id: PerformanceLinux.cpp 106061 2024-09-16 14:03:52Z vboxsync $ */
2/** @file
3 * VBox Linux-specific Performance Classes implementation.
4 */
5
6/*
7 * Copyright (C) 2008-2024 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28#define LOG_GROUP LOG_GROUP_MAIN_PERFORMANCECOLLECTOR
29#include <stdio.h>
30#include <unistd.h>
31#include <sys/statvfs.h>
32#include <errno.h>
33#include <mntent.h>
34#include <iprt/alloc.h>
35#include <iprt/cdefs.h>
36#include <iprt/ctype.h>
37#include <iprt/err.h>
38#include <iprt/param.h>
39#include <iprt/path.h>
40#include <iprt/string.h>
41#include <iprt/system.h>
42#include <iprt/mp.h>
43#include <iprt/linux/sysfs.h>
44
45#include <map>
46#include <vector>
47
48#include "LoggingNew.h"
49#include "Performance.h"
50
51#define VBOXVOLINFO_EXE_NAME "VBoxVolInfo"
52
53namespace pm {
54
55class CollectorLinux : public CollectorHAL
56{
57public:
58 CollectorLinux();
59 virtual int preCollect(const CollectorHints& hints, uint64_t /* iTick */);
60 virtual int getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available);
61 virtual int getHostFilesystemUsage(const char *name, ULONG *total, ULONG *used, ULONG *available);
62 virtual int getHostDiskSize(const char *name, uint64_t *size);
63 virtual int getProcessMemoryUsage(RTPROCESS process, ULONG *used);
64
65 virtual int getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle);
66 virtual int getRawHostNetworkLoad(const char *name, uint64_t *rx, uint64_t *tx);
67 virtual int getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms);
68 virtual int getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total);
69
70 virtual int getDiskListByFs(const char *name, DiskList& listUsage, DiskList& listLoad);
71private:
72 virtual int _getRawHostCpuLoad();
73 int getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed);
74 void getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits);
75 void addVolumeDependencies(const char *pcszVolume, DiskList& listDisks);
76 void addRaidDisks(const char *pcszDevice, DiskList& listDisks);
77 char *trimTrailingDigits(char *pszName);
78 char *trimNewline(char *pszName);
79
80 struct VMProcessStats
81 {
82 uint64_t cpuUser;
83 uint64_t cpuKernel;
84 ULONG pagesUsed;
85 };
86
87 typedef std::map<RTPROCESS, VMProcessStats> VMProcessMap;
88
89 VMProcessMap mProcessStats;
90 uint64_t mUser, mKernel, mIdle;
91 uint64_t mSingleUser, mSingleKernel, mSingleIdle;
92 uint32_t mHZ;
93 ULONG mTotalRAM;
94};
95
96CollectorHAL *createHAL()
97{
98 return new CollectorLinux();
99}
100
101// Collector HAL for Linux
102
103CollectorLinux::CollectorLinux()
104{
105 long hz = sysconf(_SC_CLK_TCK);
106 if (hz == -1)
107 {
108 LogRel(("CollectorLinux failed to obtain HZ from kernel, assuming 100.\n"));
109 mHZ = 100;
110 }
111 else
112 mHZ = (uint32_t)hz;
113 LogFlowThisFunc(("mHZ=%u\n", mHZ));
114
115 uint64_t cb;
116 int vrc = RTSystemQueryTotalRam(&cb);
117 if (RT_FAILURE(vrc))
118 mTotalRAM = 0;
119 else
120 mTotalRAM = (ULONG)(cb / 1024);
121}
122
123int CollectorLinux::preCollect(const CollectorHints& hints, uint64_t /* iTick */)
124{
125 std::vector<RTPROCESS> processes;
126 hints.getProcesses(processes);
127
128 std::vector<RTPROCESS>::iterator it;
129 for (it = processes.begin(); it != processes.end(); ++it)
130 {
131 VMProcessStats vmStats;
132 int vrc = getRawProcessStats(*it, &vmStats.cpuUser, &vmStats.cpuKernel, &vmStats.pagesUsed);
133 /* On failure, do NOT stop. Just skip the entry. Having the stats for
134 * one (probably broken) process frozen/zero is a minor issue compared
135 * to not updating many process stats and the host cpu stats. */
136 if (RT_SUCCESS(vrc))
137 mProcessStats[*it] = vmStats;
138 }
139 if (hints.isHostCpuLoadCollected() || !mProcessStats.empty())
140 {
141 _getRawHostCpuLoad();
142 }
143 return VINF_SUCCESS;
144}
145
146int CollectorLinux::_getRawHostCpuLoad()
147{
148 int vrc = VINF_SUCCESS;
149 long long unsigned uUser, uNice, uKernel, uIdle, uIowait, uIrq, uSoftirq;
150 FILE *f = fopen("/proc/stat", "r");
151
152 if (f)
153 {
154 char szBuf[128];
155 if (fgets(szBuf, sizeof(szBuf), f))
156 {
157 if (sscanf(szBuf, "cpu %llu %llu %llu %llu %llu %llu %llu",
158 &uUser, &uNice, &uKernel, &uIdle, &uIowait,
159 &uIrq, &uSoftirq) == 7)
160 {
161 mUser = uUser + uNice;
162 mKernel = uKernel + uIrq + uSoftirq;
163 mIdle = uIdle + uIowait;
164 }
165 /* Try to get single CPU stats. */
166 if (fgets(szBuf, sizeof(szBuf), f))
167 {
168 if (sscanf(szBuf, "cpu0 %llu %llu %llu %llu %llu %llu %llu",
169 &uUser, &uNice, &uKernel, &uIdle, &uIowait,
170 &uIrq, &uSoftirq) == 7)
171 {
172 mSingleUser = uUser + uNice;
173 mSingleKernel = uKernel + uIrq + uSoftirq;
174 mSingleIdle = uIdle + uIowait;
175 }
176 else
177 {
178 /* Assume that this is not an SMP system. */
179 Assert(RTMpGetCount() == 1);
180 mSingleUser = mUser;
181 mSingleKernel = mKernel;
182 mSingleIdle = mIdle;
183 }
184 }
185 else
186 vrc = VERR_FILE_IO_ERROR;
187 }
188 else
189 vrc = VERR_FILE_IO_ERROR;
190 fclose(f);
191 }
192 else
193 vrc = VERR_ACCESS_DENIED;
194
195 return vrc;
196}
197
198int CollectorLinux::getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle)
199{
200 *user = mUser;
201 *kernel = mKernel;
202 *idle = mIdle;
203 return VINF_SUCCESS;
204}
205
206int CollectorLinux::getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total)
207{
208 VMProcessMap::const_iterator it = mProcessStats.find(process);
209
210 if (it == mProcessStats.end())
211 {
212 Log (("No stats pre-collected for process %x\n", process));
213 return VERR_INTERNAL_ERROR;
214 }
215 *user = it->second.cpuUser;
216 *kernel = it->second.cpuKernel;
217 *total = mUser + mKernel + mIdle;
218 return VINF_SUCCESS;
219}
220
221int CollectorLinux::getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available)
222{
223 AssertReturn(mTotalRAM, VERR_INTERNAL_ERROR);
224 uint64_t cb;
225 int vrc = RTSystemQueryAvailableRam(&cb);
226 if (RT_SUCCESS(vrc))
227 {
228 *total = mTotalRAM;
229 *available = (ULONG)(cb / 1024);
230 *used = *total - *available;
231 }
232 return vrc;
233}
234
235int CollectorLinux::getHostFilesystemUsage(const char *path, ULONG *total, ULONG *used, ULONG *available)
236{
237 struct statvfs stats;
238
239 if (statvfs(path, &stats) == -1)
240 {
241 LogRel(("Failed to collect %s filesystem usage: errno=%d.\n", path, errno));
242 return VERR_ACCESS_DENIED;
243 }
244 uint64_t cbBlock = stats.f_frsize ? stats.f_frsize : stats.f_bsize;
245 *total = (ULONG)(cbBlock * stats.f_blocks / _1M);
246 *used = (ULONG)(cbBlock * (stats.f_blocks - stats.f_bfree) / _1M);
247 *available = (ULONG)(cbBlock * stats.f_bavail / _1M);
248
249 return VINF_SUCCESS;
250}
251
252int CollectorLinux::getHostDiskSize(const char *pszFile, uint64_t *size)
253{
254 char *pszPath = NULL;
255
256 RTStrAPrintf(&pszPath, "/sys/block/%s/size", pszFile);
257 Assert(pszPath);
258
259 int vrc = VINF_SUCCESS;
260 if (!RTLinuxSysFsExists(pszPath))
261 vrc = VERR_FILE_NOT_FOUND;
262 else
263 {
264 int64_t cSize = 0;
265 vrc = RTLinuxSysFsReadIntFile(0, &cSize, pszPath);
266 if (RT_SUCCESS(vrc))
267 *size = cSize * 512;
268 }
269 RTStrFree(pszPath);
270 return vrc;
271}
272
273int CollectorLinux::getProcessMemoryUsage(RTPROCESS process, ULONG *used)
274{
275 VMProcessMap::const_iterator it = mProcessStats.find(process);
276
277 if (it == mProcessStats.end())
278 {
279 Log (("No stats pre-collected for process %x\n", process));
280 return VERR_INTERNAL_ERROR;
281 }
282 *used = it->second.pagesUsed * (PAGE_SIZE / 1024);
283 return VINF_SUCCESS;
284}
285
286int CollectorLinux::getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed)
287{
288 int vrc = VINF_SUCCESS;
289 char *pszName;
290 pid_t pid2;
291 char c;
292 int iTmp;
293 long long unsigned int u64Tmp;
294 unsigned uTmp;
295 unsigned long ulTmp;
296 signed long ilTmp;
297 ULONG u32user, u32kernel;
298 char buf[80]; /** @todo this should be tied to max allowed proc name. */
299
300 RTStrAPrintf(&pszName, "/proc/%d/stat", process);
301 FILE *f = fopen(pszName, "r");
302 RTStrFree(pszName);
303
304 if (f)
305 {
306 if (fscanf(f, "%d %79s %c %d %d %d %d %d %u %lu %lu %lu %lu %u %u "
307 "%ld %ld %ld %ld %ld %ld %llu %lu %u",
308 &pid2, buf, &c, &iTmp, &iTmp, &iTmp, &iTmp, &iTmp, &uTmp,
309 &ulTmp, &ulTmp, &ulTmp, &ulTmp, &u32user, &u32kernel,
310 &ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &u64Tmp,
311 &ulTmp, memPagesUsed) == 24)
312 {
313 Assert((pid_t)process == pid2);
314 *cpuUser = u32user;
315 *cpuKernel = u32kernel;
316 }
317 else
318 vrc = VERR_FILE_IO_ERROR;
319 fclose(f);
320 }
321 else
322 vrc = VERR_ACCESS_DENIED;
323
324 return vrc;
325}
326
327int CollectorLinux::getRawHostNetworkLoad(const char *pszFile, uint64_t *rx, uint64_t *tx)
328{
329 char szIfName[/*IFNAMSIZ*/ 16 + 36];
330
331 RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/rx_bytes", pszFile);
332 if (!RTLinuxSysFsExists(szIfName))
333 return VERR_FILE_NOT_FOUND;
334
335 int64_t cSize = 0;
336 int vrc = RTLinuxSysFsReadIntFile(0, &cSize, szIfName);
337 if (RT_FAILURE(vrc))
338 return vrc;
339
340 *rx = cSize;
341
342 RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/tx_bytes", pszFile);
343 if (!RTLinuxSysFsExists(szIfName))
344 return VERR_FILE_NOT_FOUND;
345
346 vrc = RTLinuxSysFsReadIntFile(0, &cSize, szIfName);
347 if (RT_FAILURE(vrc))
348 return vrc;
349
350 *tx = cSize;
351 return VINF_SUCCESS;
352}
353
354int CollectorLinux::getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms)
355{
356#if 0
357 int vrc = VINF_SUCCESS;
358 char szIfName[/*IFNAMSIZ*/ 16 + 36];
359 long long unsigned int u64Busy, tmp;
360
361 RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/block/%s/stat", name);
362 FILE *f = fopen(szIfName, "r");
363 if (f)
364 {
365 if (fscanf(f, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu",
366 &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11)
367 {
368 *disk_ms = u64Busy;
369 *total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ;
370 }
371 else
372 vrc = VERR_FILE_IO_ERROR;
373 fclose(f);
374 }
375 else
376 vrc = VERR_ACCESS_DENIED;
377#else
378 int vrc = VERR_MISSING;
379 FILE *f = fopen("/proc/diskstats", "r");
380 if (f)
381 {
382 char szBuf[128];
383 while (fgets(szBuf, sizeof(szBuf), f))
384 {
385 char *pszBufName = szBuf;
386 while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
387 while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip major */
388 while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
389 while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip minor */
390 while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */
391
392 char *pszBufData = strchr(pszBufName, ' ');
393 if (!pszBufData)
394 {
395 LogRel(("CollectorLinux::getRawHostDiskLoad() failed to parse disk stats: %s\n", szBuf));
396 continue;
397 }
398 *pszBufData++ = '\0';
399 if (!strcmp(name, pszBufName))
400 {
401 long long unsigned int u64Busy, tmp;
402
403 if (sscanf(pszBufData, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu",
404 &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11)
405 {
406 *disk_ms = u64Busy;
407 *total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ;
408 vrc = VINF_SUCCESS;
409 }
410 else
411 vrc = VERR_FILE_IO_ERROR;
412 break;
413 }
414 }
415 fclose(f);
416 }
417#endif
418
419 return vrc;
420}
421
422char *CollectorLinux::trimNewline(char *pszName)
423{
424 size_t cbName = strlen(pszName);
425 if (cbName == 0)
426 return pszName;
427
428 char *pszEnd = pszName + cbName - 1;
429 while (pszEnd > pszName && *pszEnd == '\n')
430 pszEnd--;
431 pszEnd[1] = '\0';
432
433 return pszName;
434}
435
436char *CollectorLinux::trimTrailingDigits(char *pszName)
437{
438 size_t cbName = strlen(pszName);
439 if (cbName == 0)
440 return pszName;
441
442 char *pszEnd = pszName + cbName - 1;
443 while (pszEnd > pszName && (RT_C_IS_DIGIT(*pszEnd) || *pszEnd == '\n'))
444 pszEnd--;
445 pszEnd[1] = '\0';
446
447 return pszName;
448}
449
450/**
451 * Use the partition name to get the name of the disk. Any path component is stripped.
452 * if fTrimDigits is true, trailing digits are stripped as well, for example '/dev/sda5'
453 * is converted to 'sda'.
454 *
455 * @param pszDiskName Where to store the name of the disk.
456 * @param cbDiskName The size of the buffer pszDiskName points to.
457 * @param pszDevName The device name used to get the disk name.
458 * @param fTrimDigits Trim trailing digits (e.g. /dev/sda5)
459 */
460void CollectorLinux::getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits)
461{
462 unsigned cbName = 0;
463 size_t cbDevName = strlen(pszDevName);
464 const char *pszEnd = pszDevName + cbDevName - 1;
465 if (fTrimDigits)
466 while (pszEnd > pszDevName && RT_C_IS_DIGIT(*pszEnd))
467 pszEnd--;
468 while (pszEnd > pszDevName && *pszEnd != '/')
469 {
470 cbName++;
471 pszEnd--;
472 }
473 RTStrCopy(pszDiskName, RT_MIN(cbName + 1, cbDiskName), pszEnd + 1);
474}
475
476void CollectorLinux::addRaidDisks(const char *pcszDevice, DiskList& listDisks)
477{
478 FILE *f = fopen("/proc/mdstat", "r");
479 if (f)
480 {
481 char szBuf[128];
482 while (fgets(szBuf, sizeof(szBuf), f))
483 {
484 char *pszBufName = szBuf;
485
486 char *pszBufData = strchr(pszBufName, ' ');
487 if (!pszBufData)
488 {
489 LogRel(("CollectorLinux::addRaidDisks() failed to parse disk stats: %s\n", szBuf));
490 continue;
491 }
492 *pszBufData++ = '\0';
493 if (!strcmp(pcszDevice, pszBufName))
494 {
495 while (*pszBufData == ':') ++pszBufData; /* Skip delimiter */
496 while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
497 while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip status */
498 while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
499 while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip type */
500
501 while (*pszBufData != '\0')
502 {
503 while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */
504 char *pszDisk = pszBufData;
505 while (RT_C_IS_ALPHA(*pszBufData))
506 ++pszBufData;
507 if (*pszBufData)
508 {
509 *pszBufData++ = '\0';
510 listDisks.push_back(RTCString(pszDisk));
511 while (*pszBufData != '\0' && *pszBufData != ' ')
512 ++pszBufData;
513 }
514 else
515 listDisks.push_back(RTCString(pszDisk));
516 }
517 break;
518 }
519 }
520 fclose(f);
521 }
522}
523
524void CollectorLinux::addVolumeDependencies(const char *pcszVolume, DiskList& listDisks)
525{
526 /** @todo r=bird: This is presumptive and will misbehave if someone puts VBox
527 * in directory which path contains spaces or other problematic
528 * characters. This is one of the reasons to avoid popen(). */
529 static const char s_szSlashExeNameSpace[] = "/" VBOXVOLINFO_EXE_NAME " ";
530 size_t const cchVolume = strlen(pcszVolume);
531 char szVolInfo[RTPATH_MAX];
532 int vrc = RTPathAppPrivateArch(szVolInfo, sizeof(szVolInfo) - sizeof(s_szSlashExeNameSpace) - cchVolume);
533 if (RT_FAILURE(vrc))
534 {
535 LogRel(("VolInfo: Failed to get program path, vrc=%Rrc\n", vrc));
536 /** @todo r=bird: inconsistent failure behaviour. if popen fails, volume is
537 * pushed onto the list, while here it isn't. */
538 return;
539 }
540 memcpy(mempcpy(strchr(szVolInfo, '\0'), s_szSlashExeNameSpace, sizeof(s_szSlashExeNameSpace) - 1),
541 pcszVolume, cchVolume + 1);
542
543 FILE *fp = popen(szVolInfo, "r");
544 if (fp)
545 {
546 char szBuf[128];
547
548 while (fgets(szBuf, sizeof(szBuf), fp))
549 if (strncmp(szBuf, RT_STR_TUPLE("dm-")))
550 listDisks.push_back(RTCString(trimTrailingDigits(szBuf)));
551 else
552 listDisks.push_back(RTCString(trimNewline(szBuf)));
553
554 pclose(fp);
555 }
556 else
557 listDisks.push_back(RTCString(pcszVolume));
558}
559
560int CollectorLinux::getDiskListByFs(const char *pszPath, DiskList& listUsage, DiskList& listLoad)
561{
562 FILE *mtab = setmntent("/etc/mtab", "r");
563 if (mtab)
564 {
565 struct mntent *mntent;
566 while ((mntent = getmntent(mtab)))
567 {
568 /* Skip rootfs entry, there must be another root mount. */
569 if (strcmp(mntent->mnt_fsname, "rootfs") == 0)
570 continue;
571 if (strcmp(pszPath, mntent->mnt_dir) == 0)
572 {
573 char szDevName[128];
574 char szFsName[1024];
575 /* Try to resolve symbolic link if necessary. Yes, we access the file system here! */
576 int vrc = RTPathReal(mntent->mnt_fsname, szFsName, sizeof(szFsName));
577 if (RT_FAILURE(vrc))
578 continue; /* something got wrong, just ignore this path */
579 /* check against the actual mtab entry, NOT the real path as /dev/mapper/xyz is
580 * often a symlink to something else */
581 if (!strncmp(mntent->mnt_fsname, RT_STR_TUPLE("/dev/mapper")))
582 {
583 /* LVM */
584 getDiskName(szDevName, sizeof(szDevName), mntent->mnt_fsname, false /*=fTrimDigits*/);
585 addVolumeDependencies(szDevName, listUsage);
586 listLoad = listUsage;
587 }
588 else if (!strncmp(szFsName, RT_STR_TUPLE("/dev/md")))
589 {
590 /* Software RAID */
591 getDiskName(szDevName, sizeof(szDevName), szFsName, false /*=fTrimDigits*/);
592 listUsage.push_back(RTCString(szDevName));
593 addRaidDisks(szDevName, listLoad);
594 }
595 else
596 {
597 /* Plain disk partition. Trim the trailing digits to get the drive name */
598 getDiskName(szDevName, sizeof(szDevName), szFsName, true /*=fTrimDigits*/);
599 listUsage.push_back(RTCString(szDevName));
600 listLoad.push_back(RTCString(szDevName));
601 }
602 if (listUsage.empty() || listLoad.empty())
603 {
604 LogRel(("Failed to retrive disk info: getDiskName(%s) --> %s\n",
605 mntent->mnt_fsname, szDevName));
606 }
607 break;
608 }
609 }
610 endmntent(mtab);
611 }
612 return VINF_SUCCESS;
613}
614
615}
616
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette