mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-08 16:02:39 +01:00
4eb6b97097
Summary: For `PhutilSymbolLoader` queries which include `setAncestorClass()`, we need the map of which classes/interfaces things extend/implement to issue the query efficiently. Without this map, we need to load //every// class/interface and then do `is_subclass_of()`. This is doable, but not very performant if we don't have php-fpm warmup. There are a few performance-sensitive interfaces where we run queries like this, including some in Arcanist, where we'll never have warmup. This map isn't particularly difficult to generate or maintain, so just include it in symbol generation and in the library map. Also set parallelism with a flag, since it was arbitrarily hard-coded and adding flags is easy. 8 actually seems approximately optimal on my machine at least, though. Test Plan: Ran "phutil_rebuild_map.php", opened library map, got a reasonable extension map. Reviewers: vrana, btrahan Reviewed By: vrana CC: aran Maniphest Tasks: T1103 Differential Revision: https://secure.phabricator.com/D2585
518 lines
13 KiB
PHP
Executable file
518 lines
13 KiB
PHP
Executable file
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/*
|
|
* Copyright 2012 Facebook, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
require_once dirname(__FILE__).'/__init_script__.php';
|
|
|
|
$args = new PhutilArgumentParser($argv);
|
|
$args->setTagline('rebuild the library map file');
|
|
$args->setSynopsis(<<<EOHELP
|
|
**phutil_rebuild_map.php** [__options__] __root__
|
|
Rebuild the library map file for a libphutil library.
|
|
|
|
EOHELP
|
|
);
|
|
|
|
$args->parseStandardArguments();
|
|
$args->parse(
|
|
array(
|
|
array(
|
|
'name' => 'quiet',
|
|
'help' => 'Do not write status messages to stderr.',
|
|
),
|
|
array(
|
|
'name' => 'drop-cache',
|
|
'help' => 'Drop the symbol cache and rebuild the entire map from '.
|
|
'scratch.',
|
|
),
|
|
array(
|
|
'name' => 'limit',
|
|
'param' => 'N',
|
|
'default' => 8,
|
|
'help' => 'Controls the number of symbol mapper subprocesses run '.
|
|
'at once. Defaults to 8.',
|
|
),
|
|
array(
|
|
'name' => 'root',
|
|
'wildcard' => true,
|
|
)
|
|
));
|
|
|
|
$root = $args->getArg('root');
|
|
if (count($root) !== 1) {
|
|
throw new Exception("Provide exactly one library root!");
|
|
}
|
|
$root = Filesystem::resolvePath(head($root));
|
|
|
|
$builder = new PhutilLibraryMapBuilder($root);
|
|
|
|
$builder->setQuiet($args->getArg('quiet'));
|
|
$builder->setSubprocessLimit($args->getArg('limit'));
|
|
|
|
if ($args->getArg('drop-cache')) {
|
|
$builder->dropSymbolCache();
|
|
}
|
|
|
|
$builder->buildMap();
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Build maps of libphutil libraries. libphutil uses the library map to locate
|
|
* and load classes and functions in the library.
|
|
*
|
|
* @task map Mapping libphutil Libraries
|
|
* @task path Path Management
|
|
* @task symbol Symbol Analysis and Caching
|
|
* @task source Source Management
|
|
*/
|
|
final class PhutilLibraryMapBuilder {
|
|
|
|
private $root;
|
|
private $quiet;
|
|
private $subprocessLimit = 8;
|
|
|
|
const LIBRARY_MAP_VERSION_KEY = '__library_version__';
|
|
const LIBRARY_MAP_VERSION = 2;
|
|
|
|
const SYMBOL_CACHE_VERSION_KEY = '__symbol_cache_version__';
|
|
const SYMBOL_CACHE_VERSION = 2;
|
|
|
|
|
|
/* -( Mapping libphutil Libraries )---------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Create a new map builder for a library.
|
|
*
|
|
* @param string Path to the library root.
|
|
*
|
|
* @task map
|
|
*/
|
|
public function __construct($root) {
|
|
$this->root = $root;
|
|
}
|
|
|
|
|
|
/**
|
|
* Control status output. Use --quiet to set this.
|
|
*
|
|
* @param bool If true, don't show status output.
|
|
* @return this
|
|
*
|
|
* @task map
|
|
*/
|
|
public function setQuiet($quiet) {
|
|
$this->quiet = $quiet;
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Control subprocess parallelism limit. Use --limit to set this.
|
|
*
|
|
* @param int Maximum number of subprocesses to run in parallel.
|
|
* @return this
|
|
*
|
|
* @task map
|
|
*/
|
|
public function setSubprocessLimit($limit) {
|
|
$this->subprocessLimit = $limit;
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Build or rebuild the library map.
|
|
*
|
|
* @return this
|
|
*
|
|
* @task map
|
|
*/
|
|
public function buildMap() {
|
|
|
|
// Identify all the ".php" source files in the library.
|
|
$this->log("Finding source files...\n");
|
|
$source_map = $this->loadSourceFileMap();
|
|
$this->log("Found ".number_format(count($source_map))." files.\n");
|
|
|
|
|
|
// Load the symbol cache with existing parsed symbols. This allows us
|
|
// to remap libraries quickly by analyzing only changed files.
|
|
$this->log("Loading symbol cache...\n");
|
|
$symbol_cache = $this->loadSymbolCache();
|
|
|
|
|
|
// Build out the symbol analysis for all the files in the library. For
|
|
// each file, check if it's in cache. If we miss in the cache, do a fresh
|
|
// analysis.
|
|
$symbol_map = array();
|
|
$futures = array();
|
|
foreach ($source_map as $file => $hash) {
|
|
if (!empty($symbol_cache[$hash])) {
|
|
$symbol_map[$file] = $symbol_cache[$hash];
|
|
continue;
|
|
}
|
|
$futures[$file] = $this->buildSymbolAnalysisFuture($file);
|
|
}
|
|
$this->log("Found ".number_format(count($symbol_map))." files in cache.\n");
|
|
|
|
|
|
// Run the analyzer on any files which need analysis.
|
|
if ($futures) {
|
|
$limit = $this->subprocessLimit;
|
|
$count = number_format(count($futures));
|
|
|
|
$this->log("Analyzing {$count} files with {$limit} subprocesses...\n");
|
|
|
|
foreach (Futures($futures)->limit($limit) as $file => $future) {
|
|
$this->log(".");
|
|
$symbol_map[$file] = $future->resolveJSON();
|
|
}
|
|
$this->log("\nDone.\n");
|
|
}
|
|
|
|
|
|
// We're done building the cache, so write it out immediately. Note that
|
|
// we've only retained entries for files we found, so this implicitly cleans
|
|
// out old cache entries.
|
|
$this->writeSymbolCache($symbol_map, $source_map);
|
|
|
|
$this->log("Building library map...\n");
|
|
$library_map = $this->buildLibraryMap($symbol_map, $source_map);
|
|
|
|
$this->log("Writing map...\n");
|
|
$this->writeLibraryMap($library_map);
|
|
|
|
$this->log("Done.\n");
|
|
|
|
return $this;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a status message to the user, if not running in quiet mode.
|
|
*
|
|
* @param string Message to write.
|
|
* @return this
|
|
*
|
|
* @task map
|
|
*/
|
|
private function log($message) {
|
|
if (!$this->quiet) {
|
|
@fwrite(STDERR, $message);
|
|
}
|
|
return $this;
|
|
}
|
|
|
|
|
|
/* -( Path Management )---------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Get the path to some file in the library.
|
|
*
|
|
* @param string A library-relative path. If omitted, returns the library
|
|
* root path.
|
|
* @return string An absolute path.
|
|
*
|
|
* @task path
|
|
*/
|
|
private function getPath($path = '') {
|
|
return $this->root.'/'.$path;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the path to the symbol cache file.
|
|
*
|
|
* @return string Absolute path to symbol cache.
|
|
*
|
|
* @task path
|
|
*/
|
|
private function getPathForSymbolCache() {
|
|
return $this->getPath('.phutil_module_cache');
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the path to the map file.
|
|
*
|
|
* @return string Absolute path to the library map.
|
|
*
|
|
* @task path
|
|
*/
|
|
private function getPathForLibraryMap() {
|
|
return $this->getPath('__phutil_library_map__.php');
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the path to the library init file.
|
|
*
|
|
* @return string Absolute path to the library init file
|
|
*
|
|
* @task path
|
|
*/
|
|
private function getPathForLibraryInit() {
|
|
return $this->getPath('__phutil_library_init__.php');
|
|
}
|
|
|
|
|
|
/* -( Symbol Analysis and Caching )---------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Load the library symbol cache, if it exists and is readable and valid.
|
|
*
|
|
* @return dict Map of content hashes to cache of output from
|
|
* `phutil_symbols.php`.
|
|
*
|
|
* @task symbol
|
|
*/
|
|
private function loadSymbolCache() {
|
|
$cache_file = $this->getPathForSymbolCache();
|
|
|
|
try {
|
|
$cache = Filesystem::readFile($cache_file);
|
|
} catch (Exception $ex) {
|
|
$cache = null;
|
|
}
|
|
|
|
$symbol_cache = array();
|
|
if ($cache) {
|
|
$symbol_cache = json_decode($cache, true);
|
|
if (!is_array($symbol_cache)) {
|
|
$symbol_cache = array();
|
|
}
|
|
}
|
|
|
|
$version = idx($symbol_cache, self::SYMBOL_CACHE_VERSION_KEY);
|
|
if ($version != self::SYMBOL_CACHE_VERSION) {
|
|
// Throw away caches from a different version of the library.
|
|
$symbol_cache = array();
|
|
}
|
|
unset($symbol_cache[self::SYMBOL_CACHE_VERSION_KEY]);
|
|
|
|
return $symbol_cache;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a symbol map to disk cache.
|
|
*
|
|
* @param dict Symbol map of relative paths to symbols.
|
|
* @param dict Source map (like @{method:loadSourceFileMap}).
|
|
* @return void
|
|
*
|
|
* @task symbol
|
|
*/
|
|
private function writeSymbolCache(array $symbol_map, array $source_map) {
|
|
$cache_file = $this->getPathForSymbolCache();
|
|
|
|
$cache = array(
|
|
self::SYMBOL_CACHE_VERSION_KEY => self::SYMBOL_CACHE_VERSION,
|
|
);
|
|
|
|
foreach ($symbol_map as $file => $symbols) {
|
|
$cache[$source_map[$file]] = $symbols;
|
|
}
|
|
|
|
$json = json_encode($cache);
|
|
Filesystem::writeFile($cache_file, $json);
|
|
}
|
|
|
|
|
|
/**
|
|
* Drop the symbol cache, forcing a clean rebuild.
|
|
*
|
|
* @return this
|
|
*
|
|
* @task symbol
|
|
*/
|
|
public function dropSymbolCache() {
|
|
$this->log("Dropping symbol cache...\n");
|
|
Filesystem::remove($this->getPathForSymbolCache());
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a future which returns a `phutil_symbols.php` analysis of a source
|
|
* file.
|
|
*
|
|
* @param string Relative path to the source file to analyze.
|
|
* @return Future Analysis future.
|
|
*
|
|
* @task symbol
|
|
*/
|
|
private function buildSymbolAnalysisFuture($file) {
|
|
$absolute_file = $this->getPath($file);
|
|
$bin = dirname(__FILE__).'/phutil_symbols.php';
|
|
|
|
return new ExecFuture('%s --ugly -- %s', $bin, $absolute_file);
|
|
}
|
|
|
|
|
|
/* -( Source Management )-------------------------------------------------- */
|
|
|
|
|
|
/**
|
|
* Build a map of all source files in a library to hashes of their content.
|
|
* Returns an array like this:
|
|
*
|
|
* array(
|
|
* 'src/parser/ExampleParser.php' => '60b725f10c9c85c70d97880dfe8191b3',
|
|
* // ...
|
|
* );
|
|
*
|
|
* @return dict Map of library-relative paths to content hashes.
|
|
* @task source
|
|
*/
|
|
private function loadSourceFileMap() {
|
|
$root = $this->getPath();
|
|
|
|
$init = $this->getPathForLibraryInit();
|
|
if (!Filesystem::pathExists($init)) {
|
|
throw new Exception("Provided path '{$root}' is not a phutil library.");
|
|
}
|
|
|
|
$files = id(new FileFinder($root))
|
|
->withType('f')
|
|
->withSuffix('php')
|
|
->excludePath('*/.*')
|
|
->setGenerateChecksums(true)
|
|
->find();
|
|
|
|
$map = array();
|
|
foreach ($files as $file => $hash) {
|
|
if (basename($file) == '__init__.php') {
|
|
// TODO: Remove this once we kill __init__.php. This just makes the
|
|
// script run faster until we do, so testing and development is less
|
|
// annoying.
|
|
continue;
|
|
}
|
|
|
|
$file = Filesystem::readablePath($file, $root);
|
|
$file = ltrim($file, '/');
|
|
|
|
if (dirname($file) == '.') {
|
|
// We don't permit normal source files at the root level, so just ignore
|
|
// them; they're special library files.
|
|
continue;
|
|
}
|
|
|
|
$map[$file] = $hash;
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
|
|
/**
|
|
* Convert the symbol analysis of all the source files in the library into
|
|
* a library map.
|
|
*
|
|
* @param dict Symbol analysis of all source files.
|
|
* @return dict Library map.
|
|
* @task source
|
|
*/
|
|
private function buildLibraryMap(array $symbol_map) {
|
|
$library_map = array(
|
|
'class' => array(),
|
|
'function' => array(),
|
|
'xmap' => array(),
|
|
);
|
|
|
|
// Detect duplicate symbols within the library.
|
|
foreach ($symbol_map as $file => $info) {
|
|
foreach ($info['have'] as $type => $symbols) {
|
|
foreach ($symbols as $symbol => $declaration) {
|
|
$lib_type = ($type == 'interface') ? 'class' : $type;
|
|
if (!empty($library_map[$lib_type][$symbol])) {
|
|
$prior = $library_map[$lib_type][$symbol];
|
|
throw new Exception(
|
|
"Definition of {$type} '{$symbol}' in file '{$file}' duplicates ".
|
|
"prior definition in file '{$prior}'. You can not declare the ".
|
|
"same symbol twice.");
|
|
}
|
|
$library_map[$lib_type][$symbol] = $file;
|
|
}
|
|
}
|
|
$library_map['xmap'] += $info['xmap'];
|
|
}
|
|
|
|
// Simplify the common case (one parent) to make the file a little easier
|
|
// to deal with.
|
|
foreach ($library_map['xmap'] as $class => $extends) {
|
|
if (count($extends) == 1) {
|
|
$library_map['xmap'][$class] = reset($extends);
|
|
}
|
|
}
|
|
|
|
// Sort the map so it is relatively stable across changes.
|
|
foreach ($library_map as $key => $symbols) {
|
|
ksort($symbols);
|
|
$library_map[$key] = $symbols;
|
|
}
|
|
ksort($library_map);
|
|
|
|
return $library_map;
|
|
}
|
|
|
|
|
|
/**
|
|
* Write a finalized library map.
|
|
*
|
|
* @param dict Library map structure to write.
|
|
* @return void
|
|
*
|
|
* @task source
|
|
*/
|
|
private function writeLibraryMap(array $library_map) {
|
|
$map_file = $this->getPathForLibraryMap();
|
|
$version = self::LIBRARY_MAP_VERSION;
|
|
|
|
$library_map = array(
|
|
self::LIBRARY_MAP_VERSION_KEY => $version,
|
|
) + $library_map;
|
|
|
|
$library_map = var_export($library_map, $return_string = true);
|
|
$library_map = preg_replace('/\s+$/m', '', $library_map);
|
|
$library_map = preg_replace('/array \(/', 'array(', $library_map);
|
|
$at = '@';
|
|
|
|
$source_file = <<<EOPHP
|
|
<?php
|
|
|
|
/**
|
|
* This file is automatically generated. Use 'arc liberate' to rebuild it.
|
|
* {$at}generated
|
|
* {$at}phutil-library-version {$version}
|
|
*/
|
|
|
|
phutil_register_library_map({$library_map});
|
|
|
|
EOPHP;
|
|
|
|
Filesystem::writeFile($map_file, $source_file);
|
|
}
|
|
|
|
}
|