Changeset 36696 in vbox
- Timestamp:
- Apr 18, 2011 8:18:22 AM (14 years ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/VBox/Frontends/VBoxBalloonCtrl/VBoxBalloonCtrl.cpp
r36693 r36696 88 88 enum GETOPTDEF_BALLOONCTRL 89 89 { 90 GETOPTDEF_BALLOONCTRL_INC = 1000, 91 GETOPTDEF_BALLOONCTRL_DEC, 92 GETOPTDEF_BALLOONCTRL_LOWERLIMIT 90 GETOPTDEF_BALLOONCTRL_BALLOOINC = 1000, 91 GETOPTDEF_BALLOONCTRL_BALLOONDEC, 92 GETOPTDEF_BALLOONCTRL_BALLOONLOWERLIMIT, 93 GETOPTDEF_BALLOONCTRL_BALLOONMAX 93 94 }; 94 95 … … 98 99 static const RTGETOPTDEF g_aOptions[] = { 99 100 #if defined(RT_OS_DARWIN) || defined(RT_OS_LINUX) || defined (RT_OS_SOLARIS) || defined(RT_OS_FREEBSD) 100 { "--background", 'b', RTGETOPT_REQ_NOTHING },101 { "--background", 'b', RTGETOPT_REQ_NOTHING }, 101 102 #endif 102 103 /** For displayHelp(). */ 103 { "--help", 'h', RTGETOPT_REQ_NOTHING },104 { "--help", 'h', RTGETOPT_REQ_NOTHING }, 104 105 /** Sets g_ulTimeoutMS. */ 105 { "--interval", 'i', RTGETOPT_REQ_INT32 },106 { "--interval", 'i', RTGETOPT_REQ_INT32 }, 106 107 /** Sets g_ulMemoryBalloonIncrementMB. */ 107 { "--balloon-inc", GETOPTDEF_BALLOONCTRL_ INC,RTGETOPT_REQ_INT32 },108 { "--balloon-inc", GETOPTDEF_BALLOONCTRL_BALLOOINC, RTGETOPT_REQ_INT32 }, 108 109 /** Sets g_ulMemoryBalloonDecrementMB. */ 109 { "--balloon-dec", GETOPTDEF_BALLOONCTRL_DEC, RTGETOPT_REQ_INT32 }, 110 { "--balloon-lower-limit", GETOPTDEF_BALLOONCTRL_LOWERLIMIT, RTGETOPT_REQ_INT32 }, 111 { "--verbose", 'v', RTGETOPT_REQ_NOTHING } 110 { "--balloon-dec", GETOPTDEF_BALLOONCTRL_BALLOONDEC, RTGETOPT_REQ_INT32 }, 111 { "--balloon-lower-limit", GETOPTDEF_BALLOONCTRL_BALLOONLOWERLIMIT, RTGETOPT_REQ_INT32 }, 112 /** Global max. balloon limit. */ 113 { "--balloon-max", GETOPTDEF_BALLOONCTRL_BALLOONMAX, RTGETOPT_REQ_INT32 }, 114 { "--verbose", 'v', RTGETOPT_REQ_NOTHING } 112 115 }; 113 116 … … 115 118 unsigned long g_ulMemoryBalloonIncrementMB = 256; 116 119 unsigned long g_ulMemoryBalloonDecrementMB = 128; 120 /** Global balloon limit is 0, so disabled. Can be overridden by a per-VM 121 * "VBoxInternal/Guest/BalloonSizeMax" value. */ 122 unsigned long g_ulMemoryBalloonMaxMB = 0; 117 123 unsigned long g_ulLowerMemoryLimitMB = 64; 118 124 … … 129 135 { 130 136 ComPtr<IMachine> machine; 137 unsigned long ulBalloonSizeMax; 131 138 #ifndef VBOX_BALLOONCTRL_GLOBAL_PERFCOL 132 139 ComPtr<IPerformanceCollector> collector; … … 141 148 #define serviceLogVerbose(a) if (g_fVerbose) { serviceLog a; } 142 149 void serviceLog(const char *pszFormat, ...); 150 143 151 bool machineIsRunning(MachineState_T enmState); 152 mapVMIter machineGetByUUID(const Bstr &strUUID); 144 153 int machineAdd(const ComPtr<IMachine> &rptrMachine); 145 154 int machineUpdate(const ComPtr<IMachine> &rptrMachine, MachineState_T enmState); 146 int machineRemove(const Bstr &strUUID); 147 148 int updateBallooning(mapVMIterConst it); 155 int machineUpdate(mapVMIter it, MachineState_T enmState); 156 void machineRemove(mapVMIter it); 157 158 unsigned long balloonGetMaxSize(const ComPtr<IMachine> &rptrMachine); 159 bool balloonIsRequired(mapVMIter it); 160 int balloonUpdate(mapVMIterConst it); 149 161 150 162 #ifdef RT_OS_WINDOWS … … 193 205 if (SUCCEEDED(hr)) 194 206 { 195 int rc ;196 if ( fRegistered)207 int rc = RTCritSectEnter(&g_MapCritSect); 208 if (RT_SUCCESS(rc)) 197 209 { 198 ComPtr <IMachine> machine; 199 hr = g_pVirtualBox->FindMachine(uuid.raw(), machine.asOutParam()); 200 if (FAILED(hr)) 201 break; 202 rc = machineAdd(machine); 210 if (fRegistered) 211 { 212 ComPtr <IMachine> machine; 213 hr = g_pVirtualBox->FindMachine(uuid.raw(), machine.asOutParam()); 214 if (SUCCEEDED(hr)) 215 rc = machineAdd(machine); 216 else 217 rc = VERR_NOT_FOUND; 218 } 219 else 220 machineRemove(machineGetByUUID(uuid)); 221 AssertRC(rc); 222 223 int rc2 = RTCritSectLeave(&g_MapCritSect); 224 if (RT_SUCCESS(rc)) 225 rc = rc2; 203 226 } 204 else205 rc = machineRemove(uuid);206 AssertRC(rc);207 227 } 208 228 break; … … 221 241 hr = pEvent->COMGETTER(MachineId)(uuid.asOutParam()); 222 242 223 ComPtr <IMachine> machine;224 243 if (SUCCEEDED(hr)) 225 244 { 226 hr = g_pVirtualBox->FindMachine(uuid.raw(), machine.asOutParam()); 227 if (FAILED(hr)) 228 break; 229 } 230 231 if (SUCCEEDED(hr)) 232 { 233 int rc = machineUpdate(machine, machineState); 234 AssertRC(rc); 245 int rc = RTCritSectEnter(&g_MapCritSect); 246 if (RT_SUCCESS(rc)) 247 { 248 mapVMIter it = machineGetByUUID(uuid); 249 if (it == g_mapVM.end()) 250 { 251 /* Use the machine object to figure out if we 252 * need to do something. */ 253 ComPtr <IMachine> machine; 254 hr = g_pVirtualBox->FindMachine(uuid.raw(), machine.asOutParam()); 255 if (SUCCEEDED(hr)) 256 rc = machineUpdate(machine, machineState); 257 } 258 else /* Update an existing machine. */ 259 rc = machineUpdate(it, machineState); 260 AssertRC(rc); 261 262 int rc2 = RTCritSectLeave(&g_MapCritSect); 263 if (RT_SUCCESS(rc)) 264 rc = rc2; 265 } 235 266 } 236 267 break; … … 351 382 } 352 383 384 mapVMIter machineGetByUUID(const Bstr &strUUID) 385 { 386 return g_mapVM.find(strUUID); 387 } 388 353 389 int machineAdd(const ComPtr<IMachine> &rptrMachine) 354 390 { 355 int rc = RTCritSectEnter(&g_MapCritSect); 356 if (RT_SUCCESS(rc)) 357 { 358 do 359 { 360 VBOXBALLOONCTRL_MACHINE m; 361 m.machine = rptrMachine; 362 363 /* 364 * Setup metrics. 365 */ 366 com::SafeArray<BSTR> metricNames(1); 367 com::SafeIfaceArray<IUnknown> metricObjects(1); 368 com::SafeIfaceArray<IPerformanceMetric> metricAffected; 369 370 Bstr strMetricNames(L"Guest/RAM/Usage/*"); 371 strMetricNames.cloneTo(&metricNames[0]); 372 373 m.machine.queryInterfaceTo(&metricObjects[0]); 391 HRESULT rc; 392 393 do 394 { 395 VBOXBALLOONCTRL_MACHINE m; 396 m.machine = rptrMachine; 397 398 /* 399 * Setup metrics. 400 */ 401 com::SafeArray<BSTR> metricNames(1); 402 com::SafeIfaceArray<IUnknown> metricObjects(1); 403 com::SafeIfaceArray<IPerformanceMetric> metricAffected; 404 405 Bstr strMetricNames(L"Guest/RAM*"); 406 strMetricNames.cloneTo(&metricNames[0]); 407 408 m.machine.queryInterfaceTo(&metricObjects[0]); 374 409 375 410 #ifdef VBOX_BALLOONCTRL_GLOBAL_PERFCOL 376 377 378 379 380 411 CHECK_ERROR_BREAK(g_pPerfCollector, SetupMetrics(ComSafeArrayAsInParam(metricNames), 412 ComSafeArrayAsInParam(metricObjects), 413 5 /* 5 seconds */, 414 1 /* One sample is enough */, 415 ComSafeArrayAsOutParam(metricAffected))); 381 416 #else 382 CHECK_ERROR_BREAK(g_pVirtualBox, COMGETTER(PerformanceCollector)(m.collector.asOutParam())); 383 CHECK_ERROR_BREAK(m.collector, SetupMetrics(ComSafeArrayAsInParam(metricNames), 384 ComSafeArrayAsInParam(metricObjects), 385 5 /* 5 seconds */, 386 1 /* One sample is enough */, 387 ComSafeArrayAsOutParam(metricAffected))); 388 #endif 389 /* 390 * Add machine to map. 391 */ 392 Bstr strUUID; 393 CHECK_ERROR_BREAK(rptrMachine, COMGETTER(Id)(strUUID.asOutParam())); 394 395 mapVMIter it = g_mapVM.find(strUUID); 396 Assert(it == g_mapVM.end()); 397 398 g_mapVM.insert(std::make_pair(strUUID, m)); 399 400 serviceLogVerbose(("Added machine \"%s\"\n", Utf8Str(strUUID).c_str())); 401 402 } while (0); 403 404 rc = RTCritSectLeave(&g_MapCritSect); 405 } 406 407 return rc; 408 } 409 410 int machineRemove(const Bstr &strUUID) 411 { 412 int rc = RTCritSectEnter(&g_MapCritSect); 413 if (RT_SUCCESS(rc)) 414 { 417 CHECK_ERROR_BREAK(g_pVirtualBox, COMGETTER(PerformanceCollector)(m.collector.asOutParam())); 418 CHECK_ERROR_BREAK(m.collector, SetupMetrics(ComSafeArrayAsInParam(metricNames), 419 ComSafeArrayAsInParam(metricObjects), 420 5 /* 5 seconds */, 421 1 /* One sample is enough */, 422 ComSafeArrayAsOutParam(metricAffected))); 423 #endif 424 /* 425 * Add machine to map. 426 */ 427 Bstr strUUID; 428 CHECK_ERROR_BREAK(rptrMachine, COMGETTER(Id)(strUUID.asOutParam())); 429 430 if (!metricAffected.size()) 431 serviceLogVerbose(("%s: No metrics available yet!\n", Utf8Str(strUUID).c_str())); 432 415 433 mapVMIter it = g_mapVM.find(strUUID); 416 if (it != g_mapVM.end()) 417 { 418 do 419 { 420 /* 421 * Remove machine from map. 422 */ 423 g_mapVM.erase(it); 424 serviceLogVerbose(("Removed machine \"%s\"\n", Utf8Str(strUUID).c_str())); 425 426 } while (0); 427 } 428 else 429 { 430 AssertMsgFailed(("Removing non-existent machine \"%s\"!\n", 431 Utf8Str(strUUID).c_str())); 432 } 433 434 rc = RTCritSectLeave(&g_MapCritSect); 435 } 436 return rc; 434 Assert(it == g_mapVM.end()); 435 436 g_mapVM.insert(std::make_pair(strUUID, m)); 437 438 serviceLogVerbose(("Added machine \"%s\"\n", Utf8Str(strUUID).c_str())); 439 440 } while (0); 441 442 return SUCCEEDED(rc) ? VINF_SUCCESS : VERR_COM_IPRT_ERROR; /* @todo Find a better error! */ 443 } 444 445 void machineRemove(mapVMIter it) 446 { 447 if (it != g_mapVM.end()) 448 { 449 /* Must log before erasing the iterator because of the UUID ref! */ 450 serviceLogVerbose(("Removing machine \"%s\"\n", Utf8Str(it->first).c_str())); 451 452 /* 453 * Remove machine from map. 454 */ 455 g_mapVM.erase(it); 456 } 437 457 } 438 458 439 459 int machineUpdate(const ComPtr<IMachine> &rptrMachine, MachineState_T enmState) 440 460 { 441 int rc = RTCritSectEnter(&g_MapCritSect); 442 if (RT_SUCCESS(rc)) 443 { 444 Bstr strUUID; 445 HRESULT hrc = rptrMachine->COMGETTER(Id)(strUUID.asOutParam()); 446 if (SUCCEEDED(hrc)) 447 { 448 mapVMIter it = g_mapVM.find(strUUID); 449 if (it != g_mapVM.end()) 450 { 451 serviceLogVerbose(("Updating machine \"%s\" to state \"%ld\"\n", 452 Utf8Str(strUUID).c_str(), enmState)); 453 rc = updateBallooning(it); 454 } 455 } 456 457 int rc2 = RTCritSectLeave(&g_MapCritSect); 458 if (RT_SUCCESS(rc)) 459 rc = rc2; 460 } 461 462 return rc; 463 } 464 461 if ( !balloonGetMaxSize(rptrMachine) 462 || !machineIsRunning(enmState)) 463 { 464 return VINF_SUCCESS; /* Machine is not required to be added. */ 465 } 466 return machineAdd(rptrMachine); 467 } 468 469 int machineUpdate(mapVMIter it, MachineState_T enmState) 470 { 471 Assert(it != g_mapVM.end()); 472 473 if ( !balloonIsRequired(it) 474 || !machineIsRunning(enmState)) 475 { 476 machineRemove(it); 477 return VINF_SUCCESS; 478 } 479 480 return balloonUpdate(it); 481 } 465 482 466 483 int getMetric(mapVMIterConst it, const Bstr& strName, LONG *pulData) … … 500 517 ComSafeArrayAsOutParam(retLengths), 501 518 ComSafeArrayAsOutParam(retData)); 502 if (SUCCEEDED(hrc) && retData.size()) 503 *pulData = retData[retIndices[0]]; 519 #if 0 520 /* Useful for metrics debugging. */ 521 for (unsigned j = 0; j < retNames.size(); j++) 522 { 523 Bstr metricUnit(retUnits[j]); 524 Bstr metricName(retNames[j]); 525 RTPrintf("%-20ls ", metricName.raw()); 526 const char *separator = ""; 527 for (unsigned k = 0; k < retLengths[j]; k++) 528 { 529 if (retScales[j] == 1) 530 RTPrintf("%s%d %ls", separator, retData[retIndices[j] + k], metricUnit.raw()); 531 else 532 RTPrintf("%s%d.%02d%ls", separator, retData[retIndices[j] + k] / retScales[j], 533 (retData[retIndices[j] + k] * 100 / retScales[j]) % 100, metricUnit.raw()); 534 separator = ", "; 535 } 536 RTPrintf("\n"); 537 } 538 #endif 539 540 if (SUCCEEDED(hrc)) 541 *pulData = retData.size() ? retData[retIndices[0]] : 0; 504 542 505 543 return SUCCEEDED(hrc) ? VINF_SUCCESS : VINF_NOT_SUPPORTED; 506 544 } 507 545 508 /* Does not do locking! */ 509 int updateBallooning(mapVMIterConst it) 510 { 511 /* Is ballooning necessary? Only do if VM is running! */ 512 MachineState_T machineState; 513 if ( SUCCEEDED(it->second.machine->COMGETTER(State)(&machineState)) 514 && !machineIsRunning(machineState)) 515 { 516 return VINF_SUCCESS; /* Skip ballooning. */ 517 } 518 546 unsigned long balloonGetMaxSize(const ComPtr<IMachine> &rptrMachine) 547 { 548 /* 549 * Try to retrieve the balloon maximum size via the following order: 550 * - command line parameter ("--balloon-max") 551 * - per-VM parameter ("VBoxInternal/Guest/BalloonSizeMax") 552 * - global parameter ("VBoxInternal/Guest/BalloonSizeMax") 553 */ 554 unsigned long ulBalloonMax = g_ulMemoryBalloonMaxMB; /* Use global limit as default. */ 555 if (!ulBalloonMax) /* Not set by command line? */ 556 { 557 /* Try per-VM approach. */ 558 Bstr strValue; 559 HRESULT rc = rptrMachine->GetExtraData(Bstr("VBoxInternal/Guest/BalloonSizeMax").raw(), 560 strValue.asOutParam()); 561 if ( SUCCEEDED(rc) 562 && !strValue.isEmpty()) 563 { 564 ulBalloonMax = Utf8Str(strValue).toUInt32(); 565 } 566 } 567 if (!ulBalloonMax) /* Still not set by per-VM value? */ 568 { 569 /* Try global approach. */ 570 Bstr strValue; 571 HRESULT rc = g_pVirtualBox->GetExtraData(Bstr("VBoxInternal/Guest/BalloonSizeMax").raw(), 572 strValue.asOutParam()); 573 if ( SUCCEEDED(rc) 574 && !strValue.isEmpty()) 575 { 576 ulBalloonMax = Utf8Str(strValue).toUInt32(); 577 } 578 } 579 return ulBalloonMax; 580 } 581 582 bool balloonIsRequired(mapVMIter it) 583 { 584 /* Only do ballooning if we have a maximum balloon size set. */ 585 it->second.ulBalloonSizeMax = balloonGetMaxSize(it->second.machine); 586 587 return it->second.ulBalloonSizeMax ? true : false; 588 } 589 590 /* Does the actual ballooning and assumes the machine is 591 * capable and ready for ballooning. */ 592 int balloonUpdate(mapVMIterConst it) 593 { 519 594 /* 520 595 * Get metrics collected at this point. … … 525 600 vrc = getMetric(it, L"Guest/RAM/Usage/Balloon", &lBalloonCur); 526 601 527 lMemFree /= 1024;528 Assert(lMemFree > 0);529 lBalloonCur /= 1024;530 531 602 if (RT_SUCCESS(vrc)) 532 603 { 533 unsigned long ulBalloonMax = 64; /* 64 MB is the default. */ 534 Bstr strValue; 535 HRESULT rc = it->second.machine->GetExtraData(Bstr("VBoxInternal/Guest/BalloonSizeMax").raw(), 536 strValue.asOutParam()); 537 if (FAILED(rc) || strValue.isEmpty()) 538 serviceLog("Warning: Unable to get balloon size for machine \"%s\", setting to 64 MB", 539 Utf8Str(it->first).c_str()); 540 else 541 ulBalloonMax = Utf8Str(strValue).toUInt32(); 604 /* If guest statistics are not up and running yet, skip this iteration 605 * and try next time. */ 606 if (lMemFree <= 0) 607 { 608 #ifdef DEBUG 609 serviceLogVerbose(("%s: No metrics available yet!\n", Utf8Str(it->first).c_str())); 610 #endif 611 return VINF_SUCCESS; 612 } 613 614 lMemFree /= 1024; 615 lBalloonCur /= 1024; 616 617 serviceLogVerbose(("%s: Balloon: %ld, Free mem: %ld, Max ballon: %ld\n", 618 Utf8Str(it->first).c_str(), 619 lBalloonCur, lMemFree, it->second.ulBalloonSizeMax)); 542 620 543 621 /* Calculate current balloon delta. */ 544 long lDelta = getlBalloonDelta(lBalloonCur, lMemFree, ulBalloonMax); 545 546 serviceLogVerbose(("%s: Current balloon: %ld, Maximum ballon: %ld, Free memory: %ld\n", 547 Utf8Str(it->first).c_str(), lBalloonCur, ulBalloonMax, lMemFree)); 548 622 long lDelta = getlBalloonDelta(lBalloonCur, lMemFree, it->second.ulBalloonSizeMax); 549 623 if (lDelta) /* Only do ballooning if there's really smth. to change ... */ 550 624 { … … 555 629 Utf8Str(it->first).c_str(), 556 630 lDelta > 0 ? "Inflating" : "Deflating", lDelta, lBalloonCur); 631 632 HRESULT rc; 557 633 558 634 /* Open a session for the VM. */ … … 570 646 CHECK_ERROR_BREAK(guest, COMSETTER(MemoryBalloonSize)(lBalloonCur)); 571 647 else 572 serviceLog("Error: Unable to set new balloon size %ld for machine \"%s\" ",573 lBalloonCur, Utf8Str(it->first).c_str() );648 serviceLog("Error: Unable to set new balloon size %ld for machine \"%s\", rc=%Rhrc", 649 lBalloonCur, Utf8Str(it->first).c_str(), rc); 574 650 } while (0); 575 651 … … 579 655 } 580 656 else 581 serviceLog("Error: Unable to retrieve metrics for machine \"%s\" ",582 Utf8Str(it->first).c_str() );583 return VINF_SUCCESS;657 serviceLog("Error: Unable to retrieve metrics for machine \"%s\", rc=%Rrc", 658 Utf8Str(it->first).c_str(), vrc); 659 return vrc; 584 660 } 585 661 586 662 void vmListDestroy() 587 663 { 664 serviceLogVerbose(("Destroying VM list ...\n")); 665 588 666 int rc = RTCritSectEnter(&g_MapCritSect); 589 667 if (RT_SUCCESS(rc)) … … 607 685 { 608 686 vmListDestroy(); 609 g_mapVM.clear(); 687 688 serviceLogVerbose(("Building VM list ...\n")); 610 689 611 690 int rc = RTCritSectEnter(&g_MapCritSect); 612 691 if (RT_SUCCESS(rc)) 613 692 { 693 /* 694 * Make sure the list is empty. 695 */ 696 g_mapVM.clear(); 697 614 698 /* 615 699 * Get the list of all _running_ VMs … … 654 738 while (it != g_mapVM.end()) 655 739 { 656 rc = updateBallooning(it); 657 if (RT_FAILURE(rc)) 658 break; 740 MachineState_T machineState; 741 HRESULT hrc = it->second.machine->COMGETTER(State)(&machineState); 742 if (SUCCEEDED(hrc)) 743 { 744 rc = machineUpdate(it, machineState); 745 if (RT_FAILURE(rc)) 746 break; 747 } 659 748 it++; 660 749 } … … 902 991 break; 903 992 #endif 904 case GETOPTDEF_BALLOONCTRL_ INC:993 case GETOPTDEF_BALLOONCTRL_BALLOOINC: 905 994 pcszDescr = "Sets the ballooning increment in MB (256 MB)."; 906 995 break; 907 996 908 case GETOPTDEF_BALLOONCTRL_ DEC:997 case GETOPTDEF_BALLOONCTRL_BALLOONDEC: 909 998 pcszDescr = "Sets the ballooning decrement in MB (128 MB)."; 910 999 break; 911 1000 912 case GETOPTDEF_BALLOONCTRL_ LOWERLIMIT:1001 case GETOPTDEF_BALLOONCTRL_BALLOONLOWERLIMIT: 913 1002 pcszDescr = "Sets the ballooning lower limit in MB (64 MB)."; 1003 break; 1004 1005 case GETOPTDEF_BALLOONCTRL_BALLOONMAX: 1006 pcszDescr = "Sets the balloon maximum limit in MB (0 MB)."; 914 1007 break; 915 1008 } … … 970 1063 return 0; 971 1064 972 case GETOPTDEF_BALLOONCTRL_ INC:1065 case GETOPTDEF_BALLOONCTRL_BALLOOINC: 973 1066 g_ulMemoryBalloonIncrementMB = ValueUnion.u32; 974 1067 break; 975 1068 976 case GETOPTDEF_BALLOONCTRL_ DEC:1069 case GETOPTDEF_BALLOONCTRL_BALLOONDEC: 977 1070 g_ulMemoryBalloonDecrementMB = ValueUnion.u32; 978 1071 break; 979 1072 980 case GETOPTDEF_BALLOONCTRL_ LOWERLIMIT:1073 case GETOPTDEF_BALLOONCTRL_BALLOONLOWERLIMIT: 981 1074 g_ulLowerMemoryLimitMB = ValueUnion.u32; 1075 break; 1076 1077 case GETOPTDEF_BALLOONCTRL_BALLOONMAX: 1078 g_ulMemoryBalloonMaxMB = ValueUnion.u32; 982 1079 break; 983 1080
Note:
See TracChangeset
for help on using the changeset viewer.