'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; }