mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-15 11:22:40 +01:00
9875af739f
Summary: Ref T12997. See that task for more details. Briefly, an unusual dataset (where commits are mentioned hundreds of times by other commits) is causing some weird memory behavior in the daemons. Forcing PHP to GC cycles explicitly after each task completes seems to help with this, by cleaning up some of the memory between tasks. A more thorough fix might be to untangle the `$xactions` structure, but that's significantly more involved. Test Plan: - Did this locally in a controlled environment, saw an immediate collection of a 500MB `$xactions` cycle. - Put a similar change in production, memory usage seemed less to improve. It's hard to tell for sure that this does anything, but it shouldn't hurt. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12997 Differential Revision: https://secure.phabricator.com/D18657
471 lines
12 KiB
PHP
471 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
*
|
|
* @task request Request Cache
|
|
* @task immutable Immutable Cache
|
|
* @task setup Setup Cache
|
|
* @task compress Compression
|
|
*/
|
|
final class PhabricatorCaches extends Phobject {
|
|
|
|
private static $requestCache;
|
|
|
|
public static function getNamespace() {
|
|
return PhabricatorEnv::getEnvConfig('phabricator.cache-namespace');
|
|
}
|
|
|
|
private static function newStackFromCaches(array $caches) {
|
|
$caches = self::addNamespaceToCaches($caches);
|
|
$caches = self::addProfilerToCaches($caches);
|
|
return id(new PhutilKeyValueCacheStack())
|
|
->setCaches($caches);
|
|
}
|
|
|
|
/* -( Request Cache )------------------------------------------------------ */
|
|
|
|
|
|
/**
|
|
* Get a request cache stack.
|
|
*
|
|
* This cache stack is destroyed after each logical request. In particular,
|
|
* it is destroyed periodically by the daemons, while `static` caches are
|
|
* not.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Request cache stack.
|
|
*/
|
|
public static function getRequestCache() {
|
|
if (!self::$requestCache) {
|
|
self::$requestCache = new PhutilInRequestKeyValueCache();
|
|
}
|
|
return self::$requestCache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Destroy the request cache.
|
|
*
|
|
* This is called at the beginning of each logical request.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function destroyRequestCache() {
|
|
self::$requestCache = null;
|
|
|
|
// See T12997. Force the GC to run when the request cache is destroyed to
|
|
// clean up any cycles which may still be hanging around.
|
|
if (function_exists('gc_collect_cycles')) {
|
|
gc_collect_cycles();
|
|
}
|
|
}
|
|
|
|
|
|
/* -( Immutable Cache )---------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Gets an immutable cache stack.
|
|
*
|
|
* This stack trades mutability away for improved performance. Normally, it is
|
|
* APC + DB.
|
|
*
|
|
* In the general case with multiple web frontends, this stack can not be
|
|
* cleared, so it is only appropriate for use if the value of a given key is
|
|
* permanent and immutable.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best immutable stack available.
|
|
* @task immutable
|
|
*/
|
|
public static function getImmutableCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildImmutableCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build the immutable cache stack.
|
|
*
|
|
* @return list<PhutilKeyValueCache> List of caches.
|
|
* @task immutable
|
|
*/
|
|
private static function buildImmutableCaches() {
|
|
$caches = array();
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
|
|
return $caches;
|
|
}
|
|
|
|
public static function getMutableCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildMutableCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildMutableCaches() {
|
|
$caches = array();
|
|
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
|
|
return $caches;
|
|
}
|
|
|
|
public static function getMutableStructureCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildMutableStructureCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildMutableStructureCaches() {
|
|
$caches = array();
|
|
|
|
$cache = new PhabricatorKeyValueDatabaseCache();
|
|
$cache = new PhabricatorKeyValueSerializingCacheProxy($cache);
|
|
$caches[] = $cache;
|
|
|
|
return $caches;
|
|
}
|
|
|
|
/* -( Runtime Cache )------------------------------------------------------ */
|
|
|
|
|
|
/**
|
|
* Get a runtime cache stack.
|
|
*
|
|
* This stack is just APC. It's fast, it's effectively immutable, and it
|
|
* gets thrown away when the webserver restarts.
|
|
*
|
|
* This cache is suitable for deriving runtime caches, like a map of Conduit
|
|
* method names to provider classes.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best runtime stack available.
|
|
*/
|
|
public static function getRuntimeCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRuntimeCaches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
private static function buildRuntimeCaches() {
|
|
$caches = array();
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Repository Graph Cache )--------------------------------------------- */
|
|
|
|
|
|
public static function getRepositoryGraphL1Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL1Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL1Caches() {
|
|
$caches = array();
|
|
|
|
$request = new PhutilInRequestKeyValueCache();
|
|
$request->setLimit(32);
|
|
$caches[] = $request;
|
|
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
$caches[] = $apc;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
public static function getRepositoryGraphL2Cache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildRepositoryGraphL2Caches();
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
private static function buildRepositoryGraphL2Caches() {
|
|
$caches = array();
|
|
$caches[] = new PhabricatorKeyValueDatabaseCache();
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/* -( Server State Cache )------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Highly specialized cache for storing server process state.
|
|
*
|
|
* We use this cache to track initial steps in the setup phase, before
|
|
* configuration is loaded.
|
|
*
|
|
* This cache does NOT use the cache namespace (it must be accessed before
|
|
* we build configuration), and is global across all instances on the host.
|
|
*
|
|
* @return PhutilKeyValueCacheStack Best available server state cache stack.
|
|
* @task setup
|
|
*/
|
|
public static function getServerStateCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildSetupCaches('phabricator-server');
|
|
|
|
// NOTE: We are NOT adding a cache namespace here! This cache is shared
|
|
// across all instances on the host.
|
|
|
|
$caches = self::addProfilerToCaches($caches);
|
|
$cache = id(new PhutilKeyValueCacheStack())
|
|
->setCaches($caches);
|
|
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
|
|
/* -( Setup Cache )-------------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Highly specialized cache for performing setup checks. We use this cache
|
|
* to determine if we need to run expensive setup checks when the page
|
|
* loads. Without it, we would need to run these checks every time.
|
|
*
|
|
* Normally, this cache is just APC. In the absence of APC, this cache
|
|
* degrades into a slow, quirky on-disk cache.
|
|
*
|
|
* NOTE: Do not use this cache for anything else! It is not a general-purpose
|
|
* cache!
|
|
*
|
|
* @return PhutilKeyValueCacheStack Most qualified available cache stack.
|
|
* @task setup
|
|
*/
|
|
public static function getSetupCache() {
|
|
static $cache;
|
|
if (!$cache) {
|
|
$caches = self::buildSetupCaches('phabricator-setup');
|
|
$cache = self::newStackFromCaches($caches);
|
|
}
|
|
return $cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function buildSetupCaches($cache_name) {
|
|
// If this is the CLI, just build a setup cache.
|
|
if (php_sapi_name() == 'cli') {
|
|
return array();
|
|
}
|
|
|
|
// In most cases, we should have APC. This is an ideal cache for our
|
|
// purposes -- it's fast and empties on server restart.
|
|
$apc = new PhutilAPCKeyValueCache();
|
|
if ($apc->isAvailable()) {
|
|
return array($apc);
|
|
}
|
|
|
|
// If we don't have APC, build a poor approximation on disk. This is still
|
|
// much better than nothing; some setup steps are quite slow.
|
|
$disk_path = self::getSetupCacheDiskCachePath($cache_name);
|
|
if ($disk_path) {
|
|
$disk = new PhutilOnDiskKeyValueCache();
|
|
$disk->setCacheFile($disk_path);
|
|
$disk->setWait(0.1);
|
|
if ($disk->isAvailable()) {
|
|
return array($disk);
|
|
}
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function getSetupCacheDiskCachePath($name) {
|
|
// The difficulty here is in choosing a path which will change on server
|
|
// restart (we MUST have this property), but as rarely as possible
|
|
// otherwise (we desire this property to give the cache the best hit rate
|
|
// we can).
|
|
|
|
// Unfortunately, we don't have a very good strategy for minimizing the
|
|
// churn rate of the cache. We previously tried to use the parent process
|
|
// PID in some cases, but this was not reliable. See T9599 for one case of
|
|
// this.
|
|
|
|
$pid_basis = getmypid();
|
|
|
|
// If possible, we also want to know when the process launched, so we can
|
|
// drop the cache if a process restarts but gets the same PID an earlier
|
|
// process had. "/proc" is not available everywhere (e.g., not on OSX), but
|
|
// check if we have it.
|
|
$epoch_basis = null;
|
|
$stat = @stat("/proc/{$pid_basis}");
|
|
if ($stat !== false) {
|
|
$epoch_basis = $stat['ctime'];
|
|
}
|
|
|
|
$tmp_dir = sys_get_temp_dir();
|
|
|
|
$tmp_path = $tmp_dir.DIRECTORY_SEPARATOR.$name;
|
|
if (!file_exists($tmp_path)) {
|
|
@mkdir($tmp_path);
|
|
}
|
|
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
$tmp_path = $tmp_dir;
|
|
$is_ok = self::testTemporaryDirectory($tmp_path);
|
|
if (!$is_ok) {
|
|
// We can't find anywhere to write the cache, so just bail.
|
|
return null;
|
|
}
|
|
}
|
|
|
|
$tmp_name = 'setup-'.$pid_basis;
|
|
if ($epoch_basis) {
|
|
$tmp_name .= '.'.$epoch_basis;
|
|
}
|
|
$tmp_name .= '.cache';
|
|
|
|
return $tmp_path.DIRECTORY_SEPARATOR.$tmp_name;
|
|
}
|
|
|
|
|
|
/**
|
|
* @task setup
|
|
*/
|
|
private static function testTemporaryDirectory($dir) {
|
|
if (!@file_exists($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_dir($dir)) {
|
|
return false;
|
|
}
|
|
if (!@is_writable($dir)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static function addProfilerToCaches(array $caches) {
|
|
foreach ($caches as $key => $cache) {
|
|
$pcache = new PhutilKeyValueCacheProfiler($cache);
|
|
$pcache->setProfiler(PhutilServiceProfiler::getInstance());
|
|
$caches[$key] = $pcache;
|
|
}
|
|
return $caches;
|
|
}
|
|
|
|
private static function addNamespaceToCaches(array $caches) {
|
|
$namespace = self::getNamespace();
|
|
if (!$namespace) {
|
|
return $caches;
|
|
}
|
|
|
|
foreach ($caches as $key => $cache) {
|
|
$ncache = new PhutilKeyValueCacheNamespace($cache);
|
|
$ncache->setNamespace($namespace);
|
|
$caches[$key] = $ncache;
|
|
}
|
|
|
|
return $caches;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deflate a value, if deflation is available and has an impact.
|
|
*
|
|
* If the value is larger than 1KB, we have `gzdeflate()`, we successfully
|
|
* can deflate it, and it benefits from deflation, we deflate it. Otherwise
|
|
* we leave it as-is.
|
|
*
|
|
* Data can later be inflated with @{method:inflateData}.
|
|
*
|
|
* @param string String to attempt to deflate.
|
|
* @return string|null Deflated string, or null if it was not deflated.
|
|
* @task compress
|
|
*/
|
|
public static function maybeDeflateData($value) {
|
|
$len = strlen($value);
|
|
if ($len <= 1024) {
|
|
return null;
|
|
}
|
|
|
|
if (!function_exists('gzdeflate')) {
|
|
return null;
|
|
}
|
|
|
|
$deflated = gzdeflate($value);
|
|
if ($deflated === false) {
|
|
return null;
|
|
}
|
|
|
|
$deflated_len = strlen($deflated);
|
|
if ($deflated_len >= ($len / 2)) {
|
|
return null;
|
|
}
|
|
|
|
return $deflated;
|
|
}
|
|
|
|
|
|
/**
|
|
* Inflate data previously deflated by @{method:maybeDeflateData}.
|
|
*
|
|
* @param string Deflated data, from @{method:maybeDeflateData}.
|
|
* @return string Original, uncompressed data.
|
|
* @task compress
|
|
*/
|
|
public static function inflateData($value) {
|
|
if (!function_exists('gzinflate')) {
|
|
throw new Exception(
|
|
pht(
|
|
'%s is not available; unable to read deflated data!',
|
|
'gzinflate()'));
|
|
}
|
|
|
|
$value = gzinflate($value);
|
|
if ($value === false) {
|
|
throw new Exception(pht('Failed to inflate data!'));
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
|
|
}
|