advagg_bundler.module in Advanced CSS/JS Aggregation 6
Same filename and directory in other branches
Advanced aggregation bundler module.
File
advagg_bundler/advagg_bundler.moduleView source
<?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("\n SELECT\n count,\n bundle_md5,\n filename\n FROM (\n SELECT\n LPAD(COUNT(*), 8, '00000000') AS count,\n bundle_md5,\n filename_md5\n FROM\n {advagg_bundles}\n WHERE\n root = 1\n AND\n timestamp > %d\n GROUP BY\n filename_md5) AS ab\n INNER JOIN {advagg_files} AS af USING ( filename_md5 )\n ", 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;
}
Functions
Name | Description |
---|---|
advagg_bundler_advagg_filenames_alter | Implement hook_advagg_filenames_alter. |
advagg_bundler_analysis | Given a filename return a bundle key. |
advagg_bundler_get_css_selector_count | Gets the selector count of the provided files. |
advagg_bundler_menu | Implementation of hook_menu |
advagg_bundler_merge | Merge bundles together if too many where created. |
Constants
Name | Description |
---|---|
ADVAGG_BUNDLER_ACTIVE | 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. |
ADVAGG_BUNDLER_MAX_CSS | Default value of the maximum number of CSS bundles that can be generated in a single request. |
ADVAGG_BUNDLER_MAX_JS | Default value of the maximum number of JS bundles that can be generated in a single request. |
ADVAGG_BUNDLER_OUTDATED | Default value of the last used time before the bundle is considered outdated. 2 weeks in seconds. |
SELECTOR_SPLIT_VALUE | CSS selector limit in a single stylesheet on IE9 and below. |