1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-30 02:32:42 +01:00

Reduce the cost of generating default user profile images

Summary:
See PHI413. You can pre-generate these with `bin/people profileimage --all`, but they're needlessly expensive to generate.

Streamline the workflow and cache some of the cacheable parts to reduce the generation cost.

Test Plan:
  - Ran `bin/people profileimage --all` and saw cost drop from {nav 15.801s > 4.839s}.
  - Set `defaultProfileImagePHID` to `NULL` in `phabricator_user.user` and purged caches with `bin/cache purge --all`.
  - Loaded user directory.
  - Saw default images regenerate relatively quickly.

Differential Revision: https://secure.phabricator.com/D19168
This commit is contained in:
epriestley 2018-03-01 16:28:41 -08:00
parent 1f40e50f7e
commit 14fe941c34
7 changed files with 111 additions and 197 deletions

View file

@ -3044,7 +3044,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php', 'PhabricatorFilesApplicationStorageEnginePanel' => 'applications/files/applicationpanel/PhabricatorFilesApplicationStorageEnginePanel.php',
'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php', 'PhabricatorFilesBuiltinFile' => 'applications/files/builtin/PhabricatorFilesBuiltinFile.php',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeAvatarBuiltinFile.php',
'PhabricatorFilesComposeAvatarExample' => 'applications/uiexample/examples/PhabricatorFilesComposeAvatarExample.php',
'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php', 'PhabricatorFilesComposeIconBuiltinFile' => 'applications/files/builtin/PhabricatorFilesComposeIconBuiltinFile.php',
'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php',
'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php', 'PhabricatorFilesManagementCatWorkflow' => 'applications/files/management/PhabricatorFilesManagementCatWorkflow.php',
@ -8628,7 +8627,6 @@ phutil_register_library_map(array(
'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorFilesApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel',
'PhabricatorFilesBuiltinFile' => 'Phobject', 'PhabricatorFilesBuiltinFile' => 'Phobject',
'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesComposeAvatarBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesComposeAvatarExample' => 'PhabricatorUIExample',
'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile', 'PhabricatorFilesComposeIconBuiltinFile' => 'PhabricatorFilesBuiltinFile',
'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementCatWorkflow' => 'PhabricatorFilesManagementWorkflow',

View file

@ -333,8 +333,12 @@ final class PhabricatorImageTransformer extends Phobject {
return null; return null;
} }
// NOTE: Empirically, the highest compression level (9) seems to take
// up to twice as long as the default compression level (6) but produce
// only slightly smaller files (10% on avatars, 3% on screenshots).
ob_start(); ob_start();
$result = imagepng($image, null, 9); $result = imagepng($image, null, 6);
$output = ob_get_clean(); $output = ob_get_clean();
if (!$result) { if (!$result) {

View file

@ -7,8 +7,79 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
private $color; private $color;
private $border; private $border;
private $maps = array();
const VERSION = 'v1'; const VERSION = 'v1';
public function updateUser(PhabricatorUser $user) {
$username = $user->getUsername();
$image_map = $this->getMap('image');
$initial = phutil_utf8_strtoupper(substr($username, 0, 1));
$pack = $this->pickMap('pack', $username);
$icon = "alphanumeric/{$pack}/{$initial}.png";
if (!isset($image_map[$icon])) {
$icon = "alphanumeric/{$pack}/_default.png";
}
$border = $this->pickMap('border', $username);
$color = $this->pickMap('color', $username);
$data = $this->composeImage($color, $icon, $border);
$name = $this->getImageDisplayName($color, $icon, $border);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $name,
'profile' => true,
'canCDN' => true,
));
$user
->setDefaultProfileImagePHID($file->getPHID())
->setDefaultProfileImageVersion(self::VERSION)
->saveWithoutIndex();
unset($unguarded);
return $file;
}
private function getMap($map_key) {
if (!isset($this->maps[$map_key])) {
switch ($map_key) {
case 'pack':
$map = $this->newPackMap();
break;
case 'image':
$map = $this->newImageMap();
break;
case 'color':
$map = $this->newColorMap();
break;
case 'border':
$map = $this->newBorderMap();
break;
default:
throw new Exception(pht('Unknown map "%s".', $map_key));
}
$this->maps[$map_key] = $map;
}
return $this->maps[$map_key];
}
private function pickMap($map_key, $username) {
$map = $this->getMap($map_key);
$seed = $username.'_'.$map_key;
$key = PhabricatorHash::digestToRange($seed, 0, count($map) - 1);
return $map[$key];
}
public function setIcon($icon) { public function setIcon($icon) {
$this->icon = $icon; $this->icon = $icon;
return $this; return $this;
@ -46,15 +117,22 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
} }
public function getBuiltinDisplayName() { public function getBuiltinDisplayName() {
$icon = $this->getIcon(); return $this->getImageDisplayName(
$color = $this->getColor(); $this->getIcon(),
$border = implode(',', $this->getBorder()); $this->getColor(),
$this->getBorder());
}
private function getImageDisplayName($icon, $color, $border) {
$border = implode(',', $border);
return "{$icon}-{$color}-{$border}.png"; return "{$icon}-{$color}-{$border}.png";
} }
public function loadBuiltinFileData() { public function loadBuiltinFileData() {
return $this->composeImage( return $this->composeImage(
$this->getColor(), $this->getIcon(), $this->getBorder()); $this->getColor(),
$this->getIcon(),
$this->getBorder());
} }
private function composeImage($color, $image, $border) { private function composeImage($color, $image, $border) {
@ -68,7 +146,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
$color_const = hexdec(trim($color, '#')); $color_const = hexdec(trim($color, '#'));
$true_border = self::rgba2gd($border); $true_border = self::rgba2gd($border);
$image_map = self::getImageMap(); $image_map = $this->getMap('image');
$data = Filesystem::readFile($image_map[$image]); $data = Filesystem::readFile($image_map[$image]);
$img = imagecreatefromstring($data); $img = imagecreatefromstring($data);
@ -114,7 +192,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
return ($a << 24) | ($r << 16) | ($g << 8) | $b; return ($a << 24) | ($r << 16) | ($g << 8) | $b;
} }
public static function getImageMap() { private function newImageMap() {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/'; $root = $root.'/resources/builtin/alphanumeric/';
@ -131,64 +209,7 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
return $map; return $map;
} }
public function getUniqueProfileImage($username) { private function newPackMap() {
$pack_map = $this->getImagePackMap();
$image_map = $this->getImageMap();
$color_map = $this->getColorMap();
$border_map = $this->getBorderMap();
$file = phutil_utf8_strtoupper(substr($username, 0, 1));
$pack_count = count($pack_map);
$color_count = count($color_map);
$border_count = count($border_map);
$pack_seed = $username.'_pack';
$color_seed = $username.'_color';
$border_seed = $username.'_border';
$pack_key =
PhabricatorHash::digestToRange($pack_seed, 0, $pack_count - 1);
$color_key =
PhabricatorHash::digestToRange($color_seed, 0, $color_count - 1);
$border_key =
PhabricatorHash::digestToRange($border_seed, 0, $border_count - 1);
$pack = $pack_map[$pack_key];
$icon = 'alphanumeric/'.$pack.'/'.$file.'.png';
$color = $color_map[$color_key];
$border = $border_map[$border_key];
if (!isset($image_map[$icon])) {
$icon = 'alphanumeric/'.$pack.'/_default.png';
}
return array('color' => $color, 'icon' => $icon, 'border' => $border);
}
public function getUserProfileImageFile($username) {
$unique = $this->getUniqueProfileImage($username);
$composer = id(new self())
->setIcon($unique['icon'])
->setColor($unique['color'])
->setBorder($unique['border']);
$data = $composer->loadBuiltinFileData();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = PhabricatorFile::newFromFileData(
$data,
array(
'name' => $composer->getBuiltinDisplayName(),
'profile' => true,
'canCDN' => true,
));
unset($unguarded);
return $file;
}
public static function getImagePackMap() {
$root = dirname(phutil_get_library_root('phabricator')); $root = dirname(phutil_get_library_root('phabricator'));
$root = $root.'/resources/builtin/alphanumeric/'; $root = $root.'/resources/builtin/alphanumeric/';
@ -196,28 +217,24 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
->withType('d') ->withType('d')
->withFollowSymlinks(false) ->withFollowSymlinks(false)
->find(); ->find();
$map = array_values($map);
return array_values($map); return $map;
} }
public static function getBorderMap() { private function newBorderMap() {
return array(
$map = array(
array(0, 0, 0, 0), array(0, 0, 0, 0),
array(0, 0, 0, 0.3), array(0, 0, 0, 0.3),
array(255, 255, 255, 0.4), array(255, 255, 255, 0.4),
array(255, 255, 255, 0.7), array(255, 255, 255, 0.7),
); );
return $map;
} }
public static function getColorMap() { private function newColorMap() {
// // Via: http://tools.medialab.sciences-po.fr/iwanthue/
// Generated Colors
// http://tools.medialab.sciences-po.fr/iwanthue/ return array(
//
$map = array(
'#335862', '#335862',
'#2d5192', '#2d5192',
'#3c5da0', '#3c5da0',
@ -447,7 +464,6 @@ final class PhabricatorFilesComposeAvatarBuiltinFile
'#335862', '#335862',
'#335862', '#335862',
); );
return $map;
} }
} }

View file

@ -45,21 +45,10 @@ final class PhabricatorUserProfileImageCacheType
$generate_users[] = $user; $generate_users[] = $user;
} }
// Generate Files for anyone without a default $generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($generate_users as $generate_user) { foreach ($generate_users as $user) {
$generate_user_phid = $generate_user->getPHID(); $file = $generator->updateUser($user);
$generate_username = $generate_user->getUsername(); $file_phids[$user->getPHID()] = $file->getPHID();
$generate_version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
$generate_file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
->getUserProfileImageFile($generate_username);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$generate_user->setDefaultProfileImagePHID($generate_file->getPHID());
$generate_user->setDefaultProfileImageVersion($generate_version);
$generate_user->save();
unset($unguarded);
$file_phids[$generate_user_phid] = $generate_file->getPHID();
} }
if ($file_phids) { if ($file_phids) {

View file

@ -51,6 +51,7 @@ final class PhabricatorPeopleProfileImageWorkflow
} }
$version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION; $version = PhabricatorFilesComposeAvatarBuiltinFile::VERSION;
$generator = new PhabricatorFilesComposeAvatarBuiltinFile();
foreach ($iterator as $user) { foreach ($iterator as $user) {
$username = $user->getUsername(); $username = $user->getUsername();
@ -63,16 +64,13 @@ final class PhabricatorPeopleProfileImageWorkflow
} }
if ($default_phid == null || $is_force || $generate) { if ($default_phid == null || $is_force || $generate) {
$file = id(new PhabricatorFilesComposeAvatarBuiltinFile())
->getUserProfileImageFile($username);
$user->setDefaultProfileImagePHID($file->getPHID());
$user->setDefaultProfileImageVersion($version);
$user->save();
$console->writeOut( $console->writeOut(
"%s\n", "%s\n",
pht( pht(
'Generating profile image for "%s".', 'Generating profile image for "%s".',
$username)); $username));
$generator->updateUser($user);
} else { } else {
$console->writeOut( $console->writeOut(
"%s\n", "%s\n",

View file

@ -267,6 +267,10 @@ final class PhabricatorUser
return !($this->getPHID() === null); return !($this->getPHID() === null);
} }
public function saveWithoutIndex() {
return parent::save();
}
public function save() { public function save() {
if (!$this->getConduitCertificate()) { if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate()); $this->setConduitCertificate($this->generateConduitCertificate());
@ -276,7 +280,7 @@ final class PhabricatorUser
$this->setAccountSecret(Filesystem::readRandomCharacters(64)); $this->setAccountSecret(Filesystem::readRandomCharacters(64));
} }
$result = parent::save(); $result = $this->saveWithoutIndex();
if ($this->profile) { if ($this->profile) {
$this->profile->save(); $this->profile->save();

View file

@ -1,95 +0,0 @@
<?php
final class PhabricatorFilesComposeAvatarExample extends PhabricatorUIExample {
public function getName() {
return pht('Avatars');
}
public function getDescription() {
return pht('Tests various color palettes and sizes.');
}
public function getCategory() {
return pht('Technical');
}
public function renderExample() {
$request = $this->getRequest();
$viewer = $request->getUser();
$colors = PhabricatorFilesComposeAvatarBuiltinFile::getColorMap();
$packs = PhabricatorFilesComposeAvatarBuiltinFile::getImagePackMap();
$builtins = PhabricatorFilesComposeAvatarBuiltinFile::getImageMap();
$borders = PhabricatorFilesComposeAvatarBuiltinFile::getBorderMap();
$images = array();
foreach ($builtins as $builtin => $raw_file) {
$file = PhabricatorFile::loadBuiltin($viewer, $builtin);
$images[] = $file->getBestURI();
}
$content = array();
shuffle($colors);
foreach ($colors as $color) {
shuffle($borders);
$color_const = hexdec(trim($color, '#'));
$border = head($borders);
$border_color = implode(', ', $border);
$styles = array();
$styles[] = 'background-color: '.$color.';';
$styles[] = 'display: inline-block;';
$styles[] = 'height: 42px;';
$styles[] = 'width: 42px;';
$styles[] = 'border-radius: 3px;';
$styles[] = 'border: 4px solid rgba('.$border_color.');';
shuffle($images);
$png = head($images);
$image = phutil_tag(
'img',
array(
'src' => $png,
'height' => 42,
'width' => 42,
));
$tag = phutil_tag(
'div',
array(
'style' => implode(' ', $styles),
),
$image);
$content[] = phutil_tag(
'div',
array(
'class' => 'mlr mlb',
'style' => 'float: left;',
),
$tag);
}
$count = new PhutilNumber(
count($colors) * count($builtins) * count($borders));
$infoview = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
->appendChild(pht('This installation can generate %s unique '.
'avatars. You can add additional image packs in '.
'resources/builtins/alphanumeric/.', $count));
$info = phutil_tag_div('pmb', $infoview);
$view = phutil_tag_div('ml', $content);
return phutil_tag(
'div',
array(),
array(
$info,
$view,
));
}
}