476 lines
15 KiB
Text
476 lines
15 KiB
Text
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Advanced aggregation bundler module.
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Default value of the maximum number of CSS bundles that can be generated in
|
|
* a single request.
|
|
*/
|
|
define('ADVAGG_BUNDLER_MAX_CSS', 4);
|
|
|
|
/**
|
|
* Default value of the maximum number of JS bundles that can be generated in
|
|
* a single request.
|
|
*/
|
|
define('ADVAGG_BUNDLER_MAX_JS', 4);
|
|
|
|
/**
|
|
* Default value of the last used time before the bundle is considered outdated.
|
|
* 2 weeks in seconds.
|
|
*/
|
|
define('ADVAGG_BUNDLER_OUTDATED', 1209600);
|
|
|
|
/**
|
|
* Default value to see if the bundler should be active or passive. If it is
|
|
* passive, the bundler will only do analysis and not split up the aggregate.
|
|
*/
|
|
define('ADVAGG_BUNDLER_ACTIVE', TRUE);
|
|
|
|
/**
|
|
* CSS selector limit in a single stylesheet on IE9 and below.
|
|
*/
|
|
define('SELECTOR_SPLIT_VALUE', 4095);
|
|
|
|
/**
|
|
* Implementation of hook_menu
|
|
*/
|
|
function advagg_bundler_menu() {
|
|
$items = array();
|
|
$file_path = drupal_get_path('module', 'advagg_bundler');
|
|
|
|
$items['admin/settings/advagg/bundler'] = array(
|
|
'title' => 'Bundler',
|
|
'description' => 'Adjust Bundler settings.',
|
|
'page callback' => 'advagg_bundler_admin_page',
|
|
'type' => MENU_LOCAL_TASK,
|
|
'access arguments' => array('administer site configuration'),
|
|
'file path' => $file_path,
|
|
'file' => 'advagg_bundler.admin.inc',
|
|
'weight' => 10,
|
|
);
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Implement hook_advagg_filenames_alter.
|
|
*/
|
|
function advagg_bundler_advagg_filenames_alter(&$filenames) {
|
|
// Get max number of sub aggregates to create.
|
|
$max_css = variable_get('advagg_bundler_max_css', ADVAGG_BUNDLER_MAX_CSS);
|
|
$max_js = variable_get('advagg_bundler_max_js', ADVAGG_BUNDLER_MAX_JS);
|
|
|
|
$output = array();
|
|
foreach ($filenames as $values) {
|
|
// Extract values and set them.
|
|
$filetype = $values['filetype'];
|
|
$files = $values['files'];
|
|
$bundle_md5 = $values['bundle_md5'];
|
|
$cached_data_key = 'bundler_' . $bundle_md5;
|
|
|
|
// Try cache first; cache table is cache_advagg_bundle_reuse.
|
|
$cached_data = advagg_cached_bundle_get($cached_data_key, 'advagg_bundler_filenames_alter');
|
|
if (!empty($cached_data)) {
|
|
$output = array_merge($output, $cached_data);
|
|
continue;
|
|
}
|
|
|
|
// If cache miss then start to do processing.
|
|
// Set the max value based off of the filetype.
|
|
if ($filetype == 'css') {
|
|
$max = $max_css;
|
|
}
|
|
if ($filetype == 'js') {
|
|
$max = $max_js;
|
|
}
|
|
|
|
// If we are only going to have one bundle then do not do any more
|
|
// processing.
|
|
if (empty($max) || $max == 1) {
|
|
$output[] = $values;
|
|
$data = array($values);
|
|
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
|
|
continue;
|
|
}
|
|
|
|
// Load up each file.
|
|
$groupings = array();
|
|
// Preserve the order while grouping.
|
|
$last_group = '';
|
|
foreach ($files as $key => $filename) {
|
|
// Assign each file to their group.
|
|
$group = advagg_bundler_analysis($filename);
|
|
|
|
// Set $last_group if this is the first run of this foreach loop.
|
|
if (empty($last_group)) {
|
|
$last_group = $group;
|
|
}
|
|
|
|
if ($last_group == $group) {
|
|
// Include this new file into the last used group.
|
|
$groupings[$group][] = $filename;
|
|
}
|
|
else {
|
|
// In order to preserve CSS/JS execution order we need to move the last
|
|
// group to a unique name. Use the array key to make this group unique.
|
|
$groupings[$key . ' ' . $last_group] = $groupings[$last_group];
|
|
unset($groupings[$last_group]);
|
|
// Place the new file into the new group and set $last_group.
|
|
$groupings[$group][] = $filename;
|
|
$last_group = $group;
|
|
}
|
|
}
|
|
|
|
// If only one group then don't do any more processing.
|
|
if (count($groupings) == 1) {
|
|
$output[] = $values;
|
|
$data = array($values);
|
|
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
|
|
continue;
|
|
}
|
|
|
|
// Make sure we didn't go over the max; if we did merge the smallest bundles
|
|
// together.
|
|
advagg_bundler_merge($groupings, $max, $filetype);
|
|
|
|
// If only one group then don't do any more processing. The merge algorithm
|
|
// could have reduce the groupings down to one.
|
|
if (count($groupings) == 1) {
|
|
$output[] = $values;
|
|
$data = array($values);
|
|
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
|
|
continue;
|
|
}
|
|
|
|
// Set groups.
|
|
$data = array();
|
|
foreach ($groupings as $bundle) {
|
|
$values['files'] = $bundle;
|
|
$values['bundle_md5'] = md5(implode('', $bundle));
|
|
$data[] = $values;
|
|
$output[] = $values;
|
|
}
|
|
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
|
|
}
|
|
|
|
// Return groupings.
|
|
if (variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE)) {
|
|
$filenames = $output;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given a filename return a bundle key.
|
|
*
|
|
* @param $filename
|
|
* filename
|
|
* @param $force
|
|
* bypass the cache and get a fresh version of the analysis.
|
|
* @return
|
|
* string to be used for the grouping key.
|
|
*/
|
|
function advagg_bundler_analysis($filename = '', $force = FALSE) {
|
|
// Cache query in a static.
|
|
static $analysis = array();
|
|
if (empty($analysis)) {
|
|
// See if we have a cached version of this.
|
|
$count = db_result(db_query("SELECT COUNT(*) FROM {advagg_bundles} WHERE root=1"));
|
|
$cache = cache_get('advagg_bundler_analysis:' . $count);
|
|
if (empty($cache->data) || $force) {
|
|
// "Magic Query"; only needs to run once.
|
|
// Return a count of how many root bundles all files are used in. Count is
|
|
// padded with eight zeros so the count can be key sorted as a string
|
|
// without worrying about it getting put in the wrong order.
|
|
// Return the bundle_md5's value; we need something more unique than count
|
|
// when grouping together.
|
|
// Return the filename. Used for lookup.
|
|
// We join the advagg bundles and files together making sure to only use
|
|
// root bundles that have been used in the last 2 weeks. This prevents an
|
|
// old site structure from influencing new bundles.
|
|
// Grouping by the filename gives us the count and makes it so we don't
|
|
// return a lot of rows;
|
|
$result = db_query("
|
|
SELECT
|
|
count,
|
|
bundle_md5,
|
|
filename
|
|
FROM (
|
|
SELECT
|
|
LPAD(COUNT(*), 8, '00000000') AS count,
|
|
bundle_md5,
|
|
filename_md5
|
|
FROM
|
|
{advagg_bundles}
|
|
WHERE
|
|
root = 1
|
|
AND
|
|
timestamp > %d
|
|
GROUP BY
|
|
filename_md5) AS ab
|
|
INNER JOIN {advagg_files} AS af USING ( filename_md5 )
|
|
", time() - variable_get('advagg_bundler_outdated', ADVAGG_BUNDLER_OUTDATED));
|
|
|
|
// Save query into a static array with the filename as the key.
|
|
while ($row = db_fetch_array($result)) {
|
|
$analysis[$row['filename']] = $row['count'] . ' ' . $row['bundle_md5'];
|
|
}
|
|
|
|
// Invoke hook_advagg_bundler_analysis_alter() to give installed modules a
|
|
// chance to alter the analysis array.
|
|
drupal_alter('advagg_bundler_analysis', $analysis);
|
|
|
|
// Save results to the cache.
|
|
cache_set('advagg_bundler_analysis:' . $count, $analysis, 'cache', CACHE_TEMPORARY);
|
|
}
|
|
else {
|
|
$analysis = $cache->data;
|
|
}
|
|
}
|
|
|
|
// If no filename is given pass back then entire query results.
|
|
if (empty($filename)) {
|
|
return $analysis;
|
|
}
|
|
|
|
// Return a key to be used in groupings.
|
|
if (!empty($analysis[$filename])) {
|
|
return $analysis[$filename];
|
|
}
|
|
|
|
// We need to return a value that can be used as an array key if the query
|
|
// didn't give us anything.
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Merge bundles together if too many where created.
|
|
*
|
|
* This preserves the order.
|
|
*
|
|
* @param $groupings
|
|
* array of requested groups
|
|
* @param $max
|
|
* max number of grouping
|
|
*/
|
|
function advagg_bundler_merge(&$groupings, $max, $filetype) {
|
|
$group_count = count($groupings);
|
|
|
|
if (!empty($max)) {
|
|
// Keep going till array has been merged to the desired size.
|
|
while ($group_count > $max) {
|
|
// Get array sizes.
|
|
// Counts the number of files that are placed into each bundle.
|
|
$counts = array();
|
|
foreach ($groupings as $key => $values) {
|
|
$counts[$key] = count($values);
|
|
}
|
|
|
|
// Create mapping.
|
|
// Calculates the file count of potential merges. It only merges with
|
|
// neighbors in order to preserve execution order.
|
|
$map = array();
|
|
$prev_key = '';
|
|
foreach ($counts as $key => $val) {
|
|
// First run of the foreach loop; populate prev key/values and continue.
|
|
// We can't merge with the previous group in this case.
|
|
if (empty($prev_key)) {
|
|
$prev_key = $key;
|
|
$prev_val = $val;
|
|
continue;
|
|
}
|
|
|
|
// Array key ($prev_val + $val) is the file count of this new group if
|
|
// these 2 groups ($prev_key, $key) where to be merged together.
|
|
$map[] = array(
|
|
($prev_val + $val) => array($prev_key, $key),
|
|
);
|
|
$prev_key = $key;
|
|
$prev_val = $val;
|
|
}
|
|
|
|
// Get best merge candidate.
|
|
// We are looking for the smallest file count. $min is populated with a
|
|
// large number (15 bits) so it won't be selected in this process.
|
|
$min = 32767;
|
|
foreach ($map as $v) {
|
|
foreach ($v as $key => $values) {
|
|
$min = min($min, $key);
|
|
// If the min value (number of files in the proposed merged bundle) is
|
|
// the same as the key, then get the 2 bundle keys that generated this
|
|
// new min value.
|
|
if ($min == $key) {
|
|
list($first, $last) = $values;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create the new merged set.
|
|
$a = $groupings[$first];
|
|
$b = $groupings[$last];
|
|
$new_set = array_merge($a, $b);
|
|
|
|
// Rebuild the array with the new set in the correct place.
|
|
$new_groupings = array();
|
|
foreach ($groupings as $key => $files) {
|
|
if ($key == $first) {
|
|
$new_groupings[$first . ' ' . $last] = $new_set;
|
|
}
|
|
elseif ($key != $last) {
|
|
$new_groupings[$key] = $files;
|
|
}
|
|
}
|
|
$groupings = $new_groupings;
|
|
$group_count--;
|
|
}
|
|
}
|
|
|
|
// Make sure there isn't a group that has all files that don't exist or empty.
|
|
$bad_groups = array();
|
|
$counts = array();
|
|
foreach ($groupings as $key => $group) {
|
|
$missing_counter = 0;
|
|
$counts[$key] = count($group);
|
|
foreach ($group as $i => $file) {
|
|
advagg_clearstatcache(TRUE, $file);
|
|
if (!file_exists($file) || filesize($file) == 0) {
|
|
$missing_counter++;
|
|
}
|
|
}
|
|
|
|
// If all files are missing/empty in this group then it is a bad set.
|
|
if ($missing_counter == count($group)) {
|
|
$bad_groups[$key] = TRUE;
|
|
}
|
|
}
|
|
|
|
// Add the bad groups to the smallest grouping in this set.
|
|
if (!empty($bad_groups)) {
|
|
$merge_candidate_key = '';
|
|
$merge_candidate_count = 32767;
|
|
$bad_group = array();
|
|
foreach ($groupings as $key => $group) {
|
|
if (isset($bad_groups[$key])) {
|
|
// Merge all bad groups into one.
|
|
$bad_group = array_merge($bad_group, $group);
|
|
|
|
// Delete the bad group from the master set.
|
|
unset($groupings[$key]);
|
|
continue;
|
|
}
|
|
|
|
// Find the smallest good grouping.
|
|
$min = min($merge_candidate_count, count($group));
|
|
if ($min < $merge_candidate_count) {
|
|
$merge_candidate_key = $key;
|
|
$merge_candidate_count = $min;
|
|
}
|
|
}
|
|
|
|
// Move the bad files into the smallest good group.
|
|
$new_set = $groupings[$merge_candidate_key];
|
|
$new_set = array_merge($new_set, $bad_group);
|
|
$groupings[$merge_candidate_key] = $new_set;
|
|
}
|
|
|
|
// Prevent CSS selectors exceeding 4095 due to limits with IE9 and below.
|
|
if ($filetype == 'css') {
|
|
|
|
// Check each group to see if it exceeds the selector limit.
|
|
do {
|
|
$groupings_edited = FALSE;
|
|
foreach ($groupings as $key => $group) {
|
|
|
|
// Restart the selector limit check if the grouping was edited.
|
|
if ($groupings_edited) {
|
|
break;
|
|
}
|
|
|
|
$group_selector_counter = 0;
|
|
|
|
$selector_counts = advagg_bundler_get_css_selector_count($group);
|
|
|
|
for ($i = 0; $i < count($group) && !$groupings_edited; $i++) {
|
|
|
|
$selector_count = isset($selector_counts[$group[$i]]) ? $selector_counts[$group[$i]] : 0;
|
|
|
|
if ($group_selector_counter + $selector_count > SELECTOR_SPLIT_VALUE) {
|
|
$groupings_edited = TRUE;
|
|
|
|
// Divide the group.
|
|
$first_group = array_splice($group, 0, $i);
|
|
$second_group = array_splice($group, 0);
|
|
|
|
// Rebuild the array with the new set in the correct place.
|
|
$new_groupings = array();
|
|
foreach ($groupings as $k => $files) {
|
|
if ($k == $key) {
|
|
$new_groupings[$k . '_1'] = $first_group;
|
|
$new_groupings[$k . '_2'] = $second_group;
|
|
}
|
|
else {
|
|
$new_groupings[$k] = $files;
|
|
}
|
|
}
|
|
$groupings = $new_groupings;
|
|
}
|
|
else {
|
|
$group_selector_counter += $selector_count;
|
|
}
|
|
}
|
|
}
|
|
} while ($groupings_edited);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the selector count of the provided files.
|
|
*
|
|
* @param array $files
|
|
* Array of files to use.
|
|
*
|
|
* @return array
|
|
* The selector counts of each file.
|
|
*/
|
|
function advagg_bundler_get_css_selector_count($files) {
|
|
$results = array();
|
|
$placeholders = db_placeholders($files);
|
|
$result = db_query("SELECT filename, selector_count, timestamp FROM {advagg_bundler_selector_count} WHERE filename IN ($placeholders)", $files);
|
|
|
|
while ($row = db_fetch_array($result)) {
|
|
$modified = 0;
|
|
if (is_readable($row['filename'])) {
|
|
$modified = filemtime($row['filename']);
|
|
}
|
|
if ($modified > $row['timestamp']) {
|
|
$css = advagg_build_css_bundle(array($row['filename']), TRUE);
|
|
|
|
// Get the number of selectors.
|
|
// http://stackoverflow.com/questions/12567000/regex-matching-for-counting-css-selectors/12567381#12567381
|
|
$selector_count = preg_match_all('/\{.+?\}|,/s', $css, $matched);
|
|
|
|
db_query("UPDATE {advagg_bundler_selector_count} SET timestamp = %d, selector_count = %d WHERE filename LIKE '%s'", $modified, $selector_count, $row['filename']);
|
|
|
|
$results[$row['filename']] = $selector_count;
|
|
}
|
|
else {
|
|
$results[$row['filename']] = $row['selector_count'];
|
|
}
|
|
}
|
|
|
|
foreach ($files as $file) {
|
|
if (!isset($results[$file]) && file_exists($file)) {
|
|
$css = advagg_build_css_bundle(array($file), TRUE);
|
|
$selector_count = preg_match_all('/\{.+?\}|,/s', $css, $matched);
|
|
|
|
db_query("INSERT INTO {advagg_bundler_selector_count} VALUES('%s', '%s', %d, %d)", $file, md5($file), $selector_count, filemtime($file));
|
|
|
|
$results[$file] = $selector_count;
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|