Parent Directory
|
Revision Log
|
Revision Graph
#365303 which adds support for outputting an entire functional call stack from a Drupal page load into a nested krumo-styled output, also added support for dumping a full call stack to an output file so that you could do a diff of the Drupal functions called between two page loads, and some other miscellaneous cleanup
| 1 | <?php |
| 2 | // $Id: visualize_backtrace.module,v 1.1.2.2.2.1 2008/11/07 02:10:45 kentbye Exp $ |
| 3 | |
| 4 | /** |
| 5 | * Implementation of hook_menu |
| 6 | */ |
| 7 | function visualize_backtrace_menu() { |
| 8 | $items = array(); |
| 9 | |
| 10 | $items['view_source'] = array( |
| 11 | 'title' => t('source'), |
| 12 | 'page callback' => 'visualize_backtrace_view_source_code', |
| 13 | 'access arguments' => array('administer visualize backtrace'), |
| 14 | 'type' => MENU_CALLBACK, |
| 15 | ); |
| 16 | |
| 17 | $items['visualize_backtrace'] = array( |
| 18 | 'title' => t('Backtrace Graphs of Drupal Function Calls'), |
| 19 | 'page callback' => 'visualize_backtrace_generate', |
| 20 | 'access arguments' => array('administer visualize backtrace'), |
| 21 | 'type' => MENU_CALLBACK, |
| 22 | ); |
| 23 | |
| 24 | $items['view_traces'] = array( |
| 25 | 'title' => t('XDebug Traces'), |
| 26 | 'page callback' => 'visualize_backtrace_display_latest_traces', |
| 27 | 'access arguments' => array('administer visualize backtrace'), |
| 28 | 'type' => MENU_CALLBACK, |
| 29 | ); |
| 30 | |
| 31 | $items['test_xdebug'] = array( |
| 32 | 'title' => t('Test to See if XDebug is Creating Trace Files'), |
| 33 | 'page callback' => 'visualize_backtrace_test_xdebug', |
| 34 | 'access arguments' => array('administer visualize backtrace'), |
| 35 | 'type' => MENU_CALLBACK, |
| 36 | ); |
| 37 | |
| 38 | $items['admin/settings/visualize_backtrace'] = array( |
| 39 | 'title' => 'Visualize Backtrace Settings', |
| 40 | 'description' => 'Settings to display more function call information.', |
| 41 | 'page callback' => 'drupal_get_form', |
| 42 | 'page arguments' => array('visualize_backtrace_admin_settings'), |
| 43 | 'access arguments' => array('administer site configuration'), |
| 44 | 'type' => MENU_NORMAL_ITEM, |
| 45 | ); |
| 46 | |
| 47 | $items['visualize_backtrace_color_codes'] = array( |
| 48 | 'title' => t('Color Code Key for the Drupal Function Filenames'), |
| 49 | 'page callback' => 'visualize_backtrace_color_codes', |
| 50 | 'access arguments' => array('administer visualize backtrace'), |
| 51 | 'type' => MENU_CALLBACK, |
| 52 | ); |
| 53 | |
| 54 | return $items; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Implementation of hook_help(). |
| 59 | */ |
| 60 | function visualize_backtrace_help($path, $arg) { |
| 61 | switch ($path) { |
| 62 | case 'admin/settings/visualize_backtrace': |
| 63 | return '<p>'. t('First test to see if you have XDebug properly set up and generating trace files by visiting this !test_page. <br />More detailed set-up instructions can be found in the INSTALL.txt file. <br />The "Visualize Backtrace" can then be turned on via the !block page.<br />View the !colorcodepage for Drupal Functions in the Backtraces.', array('!test_page' => l(t('XDebug Test Page'), 'test_xdebug'), '!block' => l(t('block administration'), 'admin/build/block'), '!colorcodepage' => l(t('Color Code Page'), 'visualize_backtrace_color_codes'))) .'</p>'; |
| 64 | case 'view_traces': |
| 65 | return '<p>'. t('The most recent trace files are listed at the top, and are shown in reverse chronological order. If these files are stored in your /tmp directory, then they may be purged if you reboot your computer. The generated *.dot files stored in the files directory will need to be manually purged from time to time.') .'</p>'; |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * Implementation of hook_admin_settings(). |
| 71 | */ |
| 72 | function visualize_backtrace_admin_settings() { |
| 73 | $form['visualize_backtrace_execution_threshold'] = array( |
| 74 | '#type' => 'textfield', |
| 75 | '#title' => t('Highlight Drupal Functions Longer Than'), |
| 76 | '#description' => t('Enter an integer in milliseconds. Any Drupal function which takes longer than this many milliseconds will be highlighted red in the callstack table. This is also the threshold for marking the total time of the capture function and all of its children functions.'), |
| 77 | '#default_value' => variable_get('visualize_backtrace_execution_threshold', 5), |
| 78 | '#size' => 4, |
| 79 | '#maxlength' => 4, |
| 80 | ); |
| 81 | |
| 82 | $form['visualize_backtrace_contrib_module_filepath'] = array( |
| 83 | '#type' => 'textfield', |
| 84 | '#title' => t('Capture Contributed Module Functions at this Location'), |
| 85 | '#default_value' => variable_get('visualize_backtrace_contrib_module_filepath', 'sites/'), |
| 86 | '#description' => t('Enter the directory that contains the module functions that you want to capture through the shortcut link "Graph the Contributed Module Function Backtraces". The default directory will capture all contributed module functions, but it is possible to enter a specific module or theme directory here as well (i.e. "themes/garland" or "sites/all/modules/views").'), |
| 87 | '#size' => 40, |
| 88 | ); |
| 89 | |
| 90 | $form['visualize_backtrace_create_main_subsections'] = array( |
| 91 | '#type' => 'checkbox', |
| 92 | '#title' => t('Split the Main Section into Three Subsections'), |
| 93 | '#default_value' => variable_get('visualize_backtrace_create_main_subsections', 1), |
| 94 | '#description' => t("Sometimes the Main Section is too big to be plotted all at once, and this option splits up the main section into three equal sections, which makes it quicker and easier to plot in smaller segments. Turning this off will not create these three main subsection *.dot files whenever a new trace file is parsed") |
| 95 | ); |
| 96 | |
| 97 | $form['visualize_backtrace_source_code_link_location'] = array( |
| 98 | '#type' => 'select', |
| 99 | '#title' => t('Display Source Code Located at'), |
| 100 | '#default_value' => variable_get('visualize_backtrace_source_code_link_location', 'local'), |
| 101 | '#options' => array( |
| 102 | 'local' => t('Local copy'), |
| 103 | 'drupal60_cvs' => t('Drupal 6.0 CVS'), |
| 104 | ), |
| 105 | '#description' => t('Chose whether to dynamically link to locally-stored copies of source code on your computer or to the static versions from cvs.drupal.org'), |
| 106 | ); |
| 107 | |
| 108 | $form['visualize_backtrace_source_code_font_size'] = array( |
| 109 | '#type' => 'textfield', |
| 110 | '#title' => t('View Source Code Font Size'), |
| 111 | '#description' => t('The default font size for displaying the source code is 12px.'), |
| 112 | '#default_value' => variable_get('visualize_backtrace_source_code_font_size', 12), |
| 113 | '#size' => 4, |
| 114 | '#maxlength' => 4, |
| 115 | ); |
| 116 | |
| 117 | $form['visualize_backtrace_zgrviewer_location'] = array( |
| 118 | '#type' => 'textfield', |
| 119 | '#title' => t('Path Containing ZGRViewer'), |
| 120 | '#default_value' => variable_get('visualize_backtrace_zgrviewer_location', '/usr/local/bin/zgrviewer'), |
| 121 | '#description' => t('This is the unix path where the ZGRViewer is installed. The ZGRViewer is a java program that can be run from the command line to plot the *.dot flowchart visualizations. The INSTALL.txt contains more instructions for setting it up.'), |
| 122 | '#size' => 40, |
| 123 | ); |
| 124 | |
| 125 | $form['visualize_backtrace_hide_repeating_functions'] = array( |
| 126 | '#type' => 'checkbox', |
| 127 | '#title' => t('Hide Repeating Functions'), |
| 128 | '#default_value' => variable_get('visualize_backtrace_hide_repeating_functions', 1), |
| 129 | '#description' => t("Detect and consolidate repeating looping pairs of 1, 2 or 3 functions in order to make the flowcharts easier to read and quicker to load.") |
| 130 | ); |
| 131 | |
| 132 | $form['visualize_backtrace_debug_duplication'] = array( |
| 133 | '#type' => 'checkbox', |
| 134 | '#title' => t('Display debugging information for the repeating function detection algorithm'), |
| 135 | '#default_value' => variable_get('visualize_backtrace_debug_duplication', 0), |
| 136 | '#description' => t("Display some additional information in the Full Function Callstack table to help debug the visualize_backtrace_detect_repeating_functions() code. Only relevent when the Repeating Functions are being hidden.") |
| 137 | ); |
| 138 | |
| 139 | |
| 140 | return system_settings_form($form); |
| 141 | } |
| 142 | |
| 143 | |
| 144 | /** |
| 145 | * Implementation of hook_block(). |
| 146 | * |
| 147 | */ |
| 148 | function visualize_backtrace_block($op = 'list', $delta = 0) { |
| 149 | if ($op == 'list') { |
| 150 | $blocks[0]['info'] = t('View Backtrace Graphs'); |
| 151 | return $blocks; |
| 152 | } |
| 153 | else if ($op == 'view') { |
| 154 | switch ($delta) { |
| 155 | case 0: |
| 156 | if (user_access('administer visualize backtrace')) { |
| 157 | $block['subject'] = t('Visualize Backtrace'); |
| 158 | // Get the trace log number by striping out number from /tmp/trace.####.xt file. |
| 159 | $current_trace_number = explode('.', xdebug_get_tracefile_name()); |
| 160 | $current_trace_number = $current_trace_number[1]; |
| 161 | $links[] = l('Generate Backtrace Data for this Page Load', 'visualize_backtrace/'. $current_trace_number .'/hide_callstack/all_sections'); |
| 162 | $links[] .= l('View All Past Trace Files', 'view_traces'); |
| 163 | $links[] .= l('Color Code Key', 'visualize_backtrace_color_codes'); |
| 164 | $links[] .= l('Admin Settings', 'admin/settings/visualize_backtrace'); |
| 165 | } |
| 166 | if ($links) { |
| 167 | $block['content'] = theme('item_list', $links); |
| 168 | } |
| 169 | break; |
| 170 | } |
| 171 | return $block; |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Implementation of hook_perm |
| 177 | */ |
| 178 | function visualize_backtrace_perm() { |
| 179 | return array('administer visualize backtrace'); |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * Displays the Drupal source code stored on your localhost in the browswer so |
| 184 | * that you can toggle between the function call graph and the specific source |
| 185 | * code line number. It provides permalink anchors to each line of your Drupal |
| 186 | * source code along with proper PHP syntax highlighting |
| 187 | */ |
| 188 | function visualize_backtrace_view_source_code() { |
| 189 | // For security considerations, prevent the settings.php file being displayed |
| 190 | // There should be a better way to do this -- as I think it's throwing a lot of error handlers |
| 191 | //return "test"; |
| 192 | if (arg(3) != "settings.php") $handle = @fopen(arg(1) ."/". arg(2) ."/". arg(3) ."/". arg(4) ."/". arg(5) |
| 193 | ."/". arg(6) ."/". arg(7) ."/". arg(8) ."/". arg(9) ."/". arg(10) ."/", "r"); |
| 194 | if ($handle) { |
| 195 | $i = 1; |
| 196 | |
| 197 | $total_buffer = ""; |
| 198 | |
| 199 | while (!feof($handle)) { |
| 200 | $buffer = fgets($handle, 4096); |
| 201 | // Add in extra spaces before the colon so that the code will be aligned |
| 202 | // independent of extra spaces due to counter digit increases (i.e. 9 to 10) |
| 203 | $right_align = ""; |
| 204 | if ($i < 1000) $right_align = " "; |
| 205 | if ($i < 100) $right_align = " "; |
| 206 | if ($i < 10) $right_align = " "; |
| 207 | $total_buffer .= "XXX_BEGIN_XXX". $i ."XXX_END_XXX". $i . $right_align .": ". $buffer; |
| 208 | $i++; |
| 209 | } |
| 210 | |
| 211 | // Add a few more line breaks to separate the devel query info |
| 212 | $total_buffer .= "XXX_REPLACE_WITH_LINE_BREAKS_XXX"; |
| 213 | |
| 214 | // Do the PHP Syntax highlighting |
| 215 | $total_buffer = highlight_string($total_buffer, 1); |
| 216 | |
| 217 | // Get rid of extra line breaks -- via codefilter.module @codefilter_process_php() |
| 218 | $total_buffer = str_replace("\n", '', $total_buffer); |
| 219 | |
| 220 | // fix spaces -- via codefilter.module @codefilter_fix_spaces() |
| 221 | $total_buffer = preg_replace('@ (?! )@', ' ', $total_buffer); |
| 222 | |
| 223 | // Replace the placeholder XXX_BEGIN_XXX and XXX_END_XXX with anchor info (e.g. <a name ="45"></a> for line #45) |
| 224 | $total_buffer = str_replace("XXX_BEGIN_XXX", '<a name="', $total_buffer); |
| 225 | $total_buffer = str_replace("XXX_END_XXX", '"></a>', $total_buffer); |
| 226 | $total_buffer = str_replace("XXX_REPLACE_WITH_LINE_BREAKS_XXX", '<br /><br />', $total_buffer); |
| 227 | $total_buffer = '<div style="font-size: '. variable_get('visualize_backtrace_source_code_font_size', 12) .'px">'. $total_buffer .'</div>'; |
| 228 | |
| 229 | echo $total_buffer; |
| 230 | fclose($handle); |
| 231 | } |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * This is the main function that parses the trace file data into graphable |
| 236 | * flowcharts. The full function call backtrace is split into three sections: |
| 237 | * Bootstrap, Main, and Theme -- along with three optional subsections for the |
| 238 | * Main section if it is too large to graph at once. It is also possible to view |
| 239 | * the backtrace table and flowchart for a specific function |
| 240 | * |
| 241 | * Here are the URL Arguments: |
| 242 | * arg(0) = 'visualize_backtrace' initializes this fuction via menu_callback |
| 243 | * arg(1) = XDebug trace number to be parsed |
| 244 | * arg(2) = Flag to display a text table of all of the fuction calls that |
| 245 | * includes links to graph the backtraces of specific function calls |
| 246 | * arg(3) = $subsection_argument: Automatically graphs the flowchart of a |
| 247 | * specified section and displays just that portion of the callstack |
| 248 | * table. (i.e. "section_2") |
| 249 | * It also could be a flag to display either the |
| 250 | * backtrace or forwardtrace for a specific function |
| 251 | * (i.e. "show_function_backtrace" or "show_function_forwardtrace") |
| 252 | * It can also be a flag to display all of the functions referenced |
| 253 | * from a specific module filename |
| 254 | * (i.e "show_filename_backtrace" or "show_filename_forwardtrace") |
| 255 | * arg(4) = Show either the backtrace or forwardtrace of this specified function |
| 256 | * or specified filename. |
| 257 | * It could also be a flag to to display all of the functions that |
| 258 | * appear in filenames in the contributed module sites-all directory |
| 259 | * (i.e. "capture_all_contrib_functions") |
| 260 | * |
| 261 | * Quickly create the graphviz *.dot files with this splash page default view: |
| 262 | * /visualize_backtrace/1191366294/hide_callstack/all_sections/ |
| 263 | * |
| 264 | * Create the *.dot files and display all of the function calls in table form: |
| 265 | * /visualize_backtrace/1191366294/view_callstack/all_sections/ |
| 266 | * |
| 267 | * Create the *.dot files, display a subsection of the function calls in table |
| 268 | * form and automatically plot the backtrace flowchart for this section: |
| 269 | * /visualize_backtrace/1191366294/view_callstack/section_1 |
| 270 | * |
| 271 | * Create the *.dot files, display the backtraces for a specific function call, |
| 272 | * and automatically plots them |
| 273 | * /visualize_backtrace/1191366294/view_callstack/show_function_backtrace/db_query/ |
| 274 | * |
| 275 | * Create the *.dot files, display the backtraces & the forwardtraces for a |
| 276 | * specific function call, and automatically plots them. |
| 277 | * /visualize_backtrace/1191366294/view_callstack/show_function_forwardtrace/db_query/ |
| 278 | * |
| 279 | * Create the *.dot files, display the backtrace for all of the functions from |
| 280 | * a specific module filename and automatically plots them. |
| 281 | * visualize_backtrace/1191366294/view_callstack/show_filename_backtrace/includes-menu.inc |
| 282 | * |
| 283 | * Create the *.dot files, display all the backtraces for all of the functions |
| 284 | * that are called from a contributed module filename & automatically plots them |
| 285 | * visualize_backtrace/1191366294/view_callstack/show_filename_backtrace/capture_all_contrib_functions |
| 286 | * |
| 287 | * NOTE: XDebug must be installed & properly configured for this to work. |
| 288 | * See the INSTALL.txt or http://xdebug.org for more information |
| 289 | */ |
| 290 | function visualize_backtrace_generate() { |
| 291 | menu_rebuild(); |
| 292 | global $base_url; |
| 293 | |
| 294 | // The argument that is passed in is the trace number |
| 295 | $trace_number = arg(1); |
| 296 | |
| 297 | // Determine the XDebug trace file path by parsing the result of the |
| 298 | // xdebug_get_tracefile_name() function |
| 299 | $trace_file = visualize_backtrace_get_xdebug_tracefile_path(); |
| 300 | |
| 301 | // Add the trace number to duplicate the full path (e.g. $trace_file = "/tmp/trace.1189698885.xt") |
| 302 | $trace_file .= "trace.". $trace_number .".xt"; |
| 303 | |
| 304 | // After determining the XDebug tracefile from the arguments, then parse out the relevent columns with an |
| 305 | // awk shell command. The following awk variables read in the tab-separated data created by XDebug w/ the |
| 306 | // following values: $1 = level, $2 = function_number, $3 = always_zero, $4 = time_index, $5 = memory, |
| 307 | // $6 = function_name, $7 = defined_function, $8 = via_filename, |
| 308 | // if ($10 exists) then {$9 = included_file; $10 = line_number} else {$9 = line_number} |
| 309 | // See this page to verify all of the information that XDebug should be outputting: |
| 310 | // http://xdebug.org/docs/all_settings#trace_format |
| 311 | // We're interested in the Drupal function data (i.e. $3 = 0 & $7 = 1), but the awk command |
| 312 | // also appends the previous PHP function data if the Drupal function's line number = 0. |
| 313 | // This command creates a new trace file w/ "parsed" appended to the name (i.e. "/tmp/trace.1189698885.xtparsed") |
| 314 | // Here is the full command $ cat -v /tmp/trace.1191544445.xt | awk {'if($3==0 && $7==0) {level = $1; function_number = $2; function_name = $6; line_number = $9; via_filename = $8; time_index = previous_time_index; memory = previous_memory} if($3 == 0 && $7 == 1 && $10) {if($10==0) {print $1 "\t" $2 "\t" $6 "\t" $9 "\t" $10 "\t" previous_time_index "\t" previous_memory "\t" function_name "\t" level "\t" function_number "\t" via_filename "\t" line_number "\t" time_index "\t" memory} else {print $1 "\t" $2 "\t" $6 "\t" $9 "\t" $10 "\t" previous_time_index "\t" previous_memory}} if($3 == 0 && $7 == 1 && !$10) {if($9==0) {print $1 "\t" $2 "\t" $6 "\t" $8 "\t" $9 "\t" previous_time_index "\t" previous_memory "\t" function_name "\t" level "\t" function_number "\t" via_filename "\t" line_number "\t" time_index "\t" memory} else {print $1 "\t" $2 "\t" $6 "\t" $8 "\t" $9 "\t" previous_time_index "\t" previous_memory}} if($5) {previous_time_index = $4; previous_memory = $5;} if($1 > 0 && $2 > 0 && !$4) {print $1 "\t" $2}'} > /tmp/trace.1191544445.xtparsed |
| 315 | $parse_trace_command = 'cat -v '. $trace_file .' | '; |
| 316 | $parse_trace_command .= 'awk {\''; |
| 317 | // Save the data for each non-Drupal PHP function to be able to reference it if a procedural hook is called |
| 318 | // The time index and memory values are after the function has run, |
| 319 | // and so shift them to the next row where they will represent the beginning |
| 320 | // & so that the time & memory delta will be ($function_count+1)-$function_count |
| 321 | $parse_trace_command .= 'if($3==0 && $7==0) {'; |
| 322 | $parse_trace_command .= 'level = $1; function_number = $2; function_name = $6; line_number = $9; via_filename = $8; time_index = previous_time_index; memory = previous_memory'; |
| 323 | $parse_trace_command .= '} '; |
| 324 | // Check to see if this is a Drupal function (i.e. $3 = 0 & $7 = 1) |
| 325 | // If there is a value for column $10, then that means that an include file is being added, |
| 326 | // which means that $9 = via_filename & $10 = line_number |
| 327 | $parse_trace_command .= 'if($3 == 0 && $7 == 1 && $10) {'; |
| 328 | // If the line number is 0, then add in the previous PHP function data |
| 329 | $parse_trace_command .= 'if($10==0) {'; |
| 330 | $parse_trace_command .= 'print $1 "\t" $2 "\t" $6 "\t" $9 "\t" $10 "\t" previous_time_index "\t" previous_memory "\t" function_name "\t" level "\t" function_number "\t" via_filename "\t" line_number "\t" time_index "\t" memory'; |
| 331 | $parse_trace_command .= '} '; |
| 332 | $parse_trace_command .= 'else {'; |
| 333 | $parse_trace_command .= 'print $1 "\t" $2 "\t" $6 "\t" $9 "\t" $10 "\t" previous_time_index "\t" previous_memory'; |
| 334 | $parse_trace_command .= '}'; |
| 335 | $parse_trace_command .= '} '; |
| 336 | // Most of the Drupal function won't have a column $10, which means that $8 = via_filename & $9 = line_number |
| 337 | $parse_trace_command .= 'if($3 == 0 && $7 == 1 && !$10) {'; |
| 338 | // If the line number is 0, then add in the previous PHP function data |
| 339 | $parse_trace_command .= 'if($9==0) {'; |
| 340 | $parse_trace_command .= 'print $1 "\t" $2 "\t" $6 "\t" $8 "\t" $9 "\t" previous_time_index "\t" previous_memory "\t" function_name "\t" level "\t" function_number "\t" via_filename "\t" line_number "\t" time_index "\t" memory'; |
| 341 | $parse_trace_command .= '} '; |
| 342 | $parse_trace_command .= 'else {'; |
| 343 | $parse_trace_command .= 'print $1 "\t" $2 "\t" $6 "\t" $8 "\t" $9 "\t" previous_time_index "\t" previous_memory'; |
| 344 | $parse_trace_command .= '}'; |
| 345 | $parse_trace_command .= '} '; |
| 346 | // Store this row's end time and end memory |
| 347 | // It will be used as the next row's beginning time and beginning memory |
| 348 | $parse_trace_command .= 'if($5) {'; |
| 349 | $parse_trace_command .= 'previous_time_index = $4; previous_memory = $5;'; |
| 350 | $parse_trace_command .= '} '; |
| 351 | // This will print out the final Time and Memory values on the 2nd to last line |
| 352 | // It will also inadvertantly output the XDebug version info from the first line, |
| 353 | // but it will be filtered out when it's read back in |
| 354 | $parse_trace_command .= 'if($1 > 0 && $2 > 0 && !$4) {'; |
| 355 | $parse_trace_command .= 'print $1 "\t" $2'; |
| 356 | $parse_trace_command .= '}'; |
| 357 | |
| 358 | $parse_trace_command .= '\'} > '. $trace_file .'parsed'; |
| 359 | shell_exec($parse_trace_command); |
| 360 | |
| 361 | // Flowchart nodes can be hypertext linked to the specific source code line where the function calls was executed |
| 362 | // If the link location variable is set to local, then it will use the visualize_backtrace_view_source_code() callback for viewing local copies of code |
| 363 | // The "local" option is primarily for viewing local copies of code for that is being written for custom modules or core development |
| 364 | // before a CVS commit has been made. |
| 365 | // The "Drupal52_CVS" option is for creating hyperlinked SVG files that have accessible permalinks to the CVS.drupal.org source code lines |
| 366 | // Primarily for documentation purposes, and the specific version numbers for Drupal 5.2 are included in the $lookup_table() |
| 367 | $source_code_link_location = variable_get('visualize_backtrace_source_code_link_location', 'local'); |
| 368 | $lookup_table = visualize_backtrace_get_lookup_table_values($source_code_link_location); |
| 369 | |
| 370 | // intitialize variables |
| 371 | $row = 0; |
| 372 | $function_call_data = array(); |
| 373 | $function_call_data[1]['time_index'] = 0; |
| 374 | $function_call_data[1]['memory'] = 0; |
| 375 | |
| 376 | // Extraneous filename path data will be stripped out (i.e. "/Library/WebServer/Documents/d52/") by subtracting out the length of UNIX base_path |
| 377 | // TODO: Possibly replace some of these things with The Drupal core functions... |
| 378 | $unix_base_path = shell_exec("pwd"); |
| 379 | |
| 380 | // Open up the parsed trace file |
| 381 | $handle = fopen($trace_file ."parsed", "r"); |
| 382 | while (($data = fgetcsv($handle, 1000, "\t")) !== FALSE) { |
| 383 | // If there is a fifth column then that means that the Line = 0 for this function because it was called by |
| 384 | // a previously-called PHP function -- usually either preg_replace_callback or call_user_func_array |
| 385 | $data_length = count($data); |
| 386 | // If a row has 14 columns, then it has line = 8 and need to save the PHP function first. |
| 387 | // The first row of data also has 14 columns, but the last 9 columns are blank |
| 388 | if ($row > 0 && $data_length > 8) { |
| 389 | // Add the PHP function data to the function call list only if it is a previous level than the Drupal Function |
| 390 | // Also don't add the previous PHP function if the function is an error_handler |
| 391 | if ($data[8] < $data[0] && $data[2] != "error_handler") { |
| 392 | // First add in an additional row for the PHP function so that the levels will be plotted out correctly |
| 393 | $row++; |
| 394 | $function_call_data[$row]['PHP_function'] = TRUE; |
| 395 | $function_call_data[$row]['level'] = $data[8]; |
| 396 | $function_call_data[$row]['function_number'] = $data[9]; |
| 397 | // This is the PHP function name |
| 398 | $function_call_data[$row]['function_name'] = $data[7]; |
| 399 | // Strip out the base path info from the via_filename in order to normalize the file path for the lookup arrays |
| 400 | $function_call_data[$row]['via_filename'] = substr($data[10], (strlen($data[10])-strlen($unix_base_path))*-1); |
| 401 | $function_call_data[$row]['line_number'] = $data[11]; |
| 402 | $function_call_data[$row]['time_index'] = $data[12]; |
| 403 | $function_call_data[$row]['memory'] = $data[13]; |
| 404 | $function_call_data[$row]['via_PHP_function'] = ""; |
| 405 | } |
| 406 | |
| 407 | // Then add in the Drupal function |
| 408 | $row++; |
| 409 | $function_call_data[$row]['PHP_function'] = FALSE; |
| 410 | $function_call_data[$row]['level'] = $data[0]; |
| 411 | $function_call_data[$row]['function_number'] = $data[1]; |
| 412 | $function_call_data[$row]['function_name'] = $data[2]; |
| 413 | $function_call_data[$row]['via_filename'] = substr($data[3], (strlen($data[3])-strlen($unix_base_path))*-1); |
| 414 | $function_call_data[$row]['line_number'] = $data[4]; |
| 415 | $function_call_data[$row]['time_index'] = $data[5]; |
| 416 | $function_call_data[$row]['memory'] = $data[6]; |
| 417 | // Add data that will later be used as a label to indicate that this function was called from a PHP function |
| 418 | $function_call_data[$row]['via_PHP_function']='\n* via: '; |
| 419 | |
| 420 | } |
| 421 | else { |
| 422 | // Add in the metadata for the Drupal functions called by other Drupal functions |
| 423 | // Filter out eval function names since the drupal_eval code yields irregular trace file data |
| 424 | // The final time and memory values only have two columns, and so make sure the fourth column has a number |
| 425 | // Also filter out the the erroneous "Version:" info on the first line of the parsed file |
| 426 | //if ($data[2] != "eval" && $data[0] != "Version:" && is_numeric($data[4])) { |
| 427 | if ($data_length > 2) { |
| 428 | if ($data[2] != "eval") { |
| 429 | $row++; |
| 430 | $function_call_data[$row]['PHP_function'] = FALSE; |
| 431 | $function_call_data[$row]['level'] = $data[0]; |
| 432 | $function_call_data[$row]['function_number'] = $data[1]; |
| 433 | $function_call_data[$row]['function_name'] = $data[2]; |
| 434 | $function_call_data[$row]['via_filename'] = substr($data[3], (strlen($data[3])-strlen($unix_base_path))*-1); |
| 435 | $function_call_data[$row]['line_number'] = $data[4]; |
| 436 | $function_call_data[$row]['time_index'] = $data[5]; |
| 437 | $function_call_data[$row]['memory'] = $data[6]; |
| 438 | $function_call_data[$row]['via_PHP_function']=""; |
| 439 | } |
| 440 | } |
| 441 | elseif ($data[0] != "Version:") { |
| 442 | $final_time = $data[0]; |
| 443 | $final_memory = $data[1]; |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | // Save the total number of $rows to loop through after the duplicate functions have been removed. |
| 449 | $total_rows = $row; |
| 450 | |
| 451 | // Close the parsed trace file. |
| 452 | fclose($handle); |
| 453 | |
| 454 | // Detect whenever 1, 2 or 3 functions iteratively repeat |
| 455 | // The function all data is returned with a 'function_repeat' flag set to 1 if |
| 456 | // the function is part of a single, double or triple iterating loop |
| 457 | // Added an admin option to determine whether repeating functions should be filtered out |
| 458 | if (variable_get('visualize_backtrace_hide_repeating_functions', 1)) { |
| 459 | $function_call_data = visualize_backtrace_detect_repeating_functions($function_call_data); |
| 460 | } |
| 461 | |
| 462 | // Initialize Variables |
| 463 | // Check arg(3) to see if the backtrace for a specific function should be graphed and output into a table |
| 464 | $subsection_argument = arg(3); |
| 465 | if ($subsection_argument == "show_function_backtrace" || $subsection_argument == "show_function_forwardtrace") { |
| 466 | $capture_function = arg(4); |
| 467 | $capture_function_backtrace_flag = TRUE; |
| 468 | $subgraph_cluster_count = 0; |
| 469 | $capture_function_count = 0; |
| 470 | if ($subsection_argument == "show_function_forwardtrace") { |
| 471 | $capture_function_forwardtrace_flag = TRUE; |
| 472 | } |
| 473 | $capture_contrib_functions = FALSE; |
| 474 | // Make sure that functions with null filenames don't get marked as a capture function |
| 475 | $capture_filename = "xxx"; |
| 476 | } |
| 477 | // Use the same capture_function codebase to capture all instances of a specific module filename |
| 478 | elseif ($subsection_argument == "show_filename_backtrace" || $subsection_argument == "show_filename_forwardtrace") { |
| 479 | // Restore the '/' filepath argument delimiters by replacing any inserted '-' delimiters |
| 480 | $capture_filename = preg_replace('/-/', '/', arg(4)); |
| 481 | // Check to see if a special flag has been set in arg(4) to capture all contributed functions |
| 482 | $capture_contrib_functions = FALSE; |
| 483 | if ($capture_filename == 'capture_all_contrib_functions') { |
| 484 | // This variable can be defined in the admin section |
| 485 | $contrib_module_filepath = variable_get('visualize_backtrace_contrib_module_filepath', 'sites/'); |
| 486 | $capture_contrib_functions = TRUE; |
| 487 | } |
| 488 | $capture_function_backtrace_flag = TRUE; |
| 489 | $subgraph_cluster_count = 0; |
| 490 | $capture_function_count = 0; |
| 491 | if ($subsection_argument == "show_filename_forwardtrace") { |
| 492 | $capture_function_forwardtrace_flag = TRUE; |
| 493 | } |
| 494 | } |
| 495 | else { |
| 496 | $capture_function_backtrace_flag = FALSE; |
| 497 | $capture_contrib_functions = FALSE; |
| 498 | } |
| 499 | |
| 500 | $function_call_stack = array(); |
| 501 | // Create a 0-level index value for the function call stack to connect level=1 functions at the end |
| 502 | $function_call_stack[0] = "index.php"; |
| 503 | |
| 504 | // Keep track of whether or not the theme() function was executed from index.php |
| 505 | $index_theme_executed = FALSE; |
| 506 | |
| 507 | // Intialize the *.dot output arrays |
| 508 | $output_to_dot_all = array(); |
| 509 | $output_to_dot_function = array(); |
| 510 | |
| 511 | $row=1; |
| 512 | $function_count = 0; |
| 513 | |
| 514 | // Loop through the results to store the time and memory values in an array with a $key of the $function_count |
| 515 | // This makes it possible to calculate the deltas with ($function_count + 1) without knowing |
| 516 | // which future $row number is the next non-repeating function |
| 517 | // (i.e. $time_delta = $time_index_array[($function_count+1)] - $time_index_array[$function_count] ) |
| 518 | while ($row <= $total_rows) { |
| 519 | if (!$function_call_data[$row]['function_repeat']) { |
| 520 | $function_count++; |
| 521 | $time_index_array[$function_count] = $function_call_data[$row]['time_index']; |
| 522 | $memory_array[$function_count] = $function_call_data[$row]['memory']; |
| 523 | } |
| 524 | $row++; |
| 525 | } |
| 526 | // Add the final time and memory values at the end of the array in order to properly calculate the last function's values |
| 527 | $time_index_array[($function_count+1)] = $final_time; |
| 528 | $memory_array[($function_count+1)] = $final_memory; |
| 529 | |
| 530 | // Re-Initialize the $row & $function_count |
| 531 | $row=1; |
| 532 | $function_count = 0; |
| 533 | $capture_function_depth = 1; |
| 534 | $drupal_version = floor(VERSION); |
| 535 | $time_threshold = variable_get('visualize_backtrace_execution_threshold', 5); |
| 536 | |
| 537 | /** |
| 538 | * Generate the *.dot syntax for each function call by looping through all of |
| 539 | * the nodes & skipping over the repeating functions |
| 540 | * Optionally capture the backtrace or forwardtrace data for specific functions |
| 541 | */ |
| 542 | while ($row <= $total_rows) { |
| 543 | // Verify that this row has not been flagged as a repeating function |
| 544 | if (!$function_call_data[$row]['function_repeat']) { |
| 545 | $level = $function_call_data[$row]['level']; |
| 546 | $function_number = $function_call_data[$row]['function_number']; |
| 547 | $function_name = $function_call_data[$row]['function_name']; |
| 548 | $via_filename = $function_call_data[$row]['via_filename']; |
| 549 | $line_number = $function_call_data[$row]['line_number']; |
| 550 | $via_PHP_function = $function_call_data[$row]['via_PHP_function']; |
| 551 | $repeating_function_label = $function_call_data[$row]['repeating_function_label']; |
| 552 | |
| 553 | // This function count counts the number of non-repeating functions so that it can be shown on the flowchart as |
| 554 | // "f" plus the function count number. |
| 555 | // The function count also serves as the array index for the output data to be written to the *.dot file. |
| 556 | $function_count++; |
| 557 | $function_call_data[$row]['function_count'] = $function_count; |
| 558 | |
| 559 | // If this was a procedural Drupal hook function, then use the previous level's function_id to determine |
| 560 | // the correct via_filename & line_number -- as well as correct referential function_name for the via_PHP_function label |
| 561 | if ($via_PHP_function) { |
| 562 | // The function id is delimited by a double underscore so explode it to determine the correct PHP reference info |
| 563 | $previous_level_function_id = $output = explode('__', $function_call_stack[$level-1]); |
| 564 | $via_filename = $previous_level_function_id[1]; |
| 565 | $line_number = $previous_level_function_id[2]; |
| 566 | // Tack on the correct previous function to the via_PHP_function label |
| 567 | $via_PHP_function .= $previous_level_function_id[0]; |
| 568 | } |
| 569 | |
| 570 | // The function_id uniquely identifies this function by combining the Function name + filename reference + |
| 571 | // line number + function call count. The function call count prevents commonly run functions from making |
| 572 | // the output both clustered and more complicated to follow |
| 573 | // Functions with a line number of 0 use the line number of the previous PHP function to make it easier |
| 574 | // to link to the source code location where procedural hooks are called |
| 575 | $function_id = $function_name ."__". $via_filename ."__". $line_number ."__". $function_count; |
| 576 | |
| 577 | // This array keeps track of the call stack tree for any given function call. |
| 578 | // It is used for creating the nodal connections |
| 579 | $function_call_stack[$level] = $function_id; |
| 580 | |
| 581 | // The node color & the source code Links Permalinks for each filename |
| 582 | // If the CVS permalink w/ version number isn't defined, then either manually add it to the |
| 583 | // $lookup_table array in visualize_backtrace_get_lookup_table_values() section of code. |
| 584 | // Otherwise, it will default to the local copy, but the lines won't be accessible for |
| 585 | // documentation purposes (i.e. in creating a hypertext linked SVG) |
| 586 | if ($source_code_link_location == "local") { |
| 587 | $lookup_table[$via_filename]["source_code_line_url"] = $base_url ."/view_source/". $via_filename ."#"; |
| 588 | } |
| 589 | else { |
| 590 | if ($lookup_table[$via_filename]["source_code_line_url"] == "") { |
| 591 | $lookup_table[$via_filename]["source_code_line_url"] = $base_url ."/view_source/". $via_filename ."#"; |
| 592 | } |
| 593 | } |
| 594 | |
| 595 | // The node connection colors are sequentially alternated in order to help match the correct |
| 596 | // label to each line, which helps in cluttered graphs. |
| 597 | // Get the last digit of the function_count to determine the color for the node connection |
| 598 | $node_connection_color = $lookup_table["node_connection_color"][$function_count % 10]; |
| 599 | |
| 600 | // Create the node connection data for each row. |
| 601 | // The *.dot syntax will end up looking like the following: |
| 602 | // "{main}_1_1" -> "require_once__index.php_12_2" [label="f2" href="http://api.drupal.org/api/function/require_once/5" color="#FF0000" fontcolor="#FF0000"]; |
| 603 | $node_connection_data = '"'. $function_call_stack[$level-1] .'" -> "'. $function_id .'" [label="f'. $function_count .'" href="http://api.drupal.org/api/function/'. |
| 604 | $function_name .'/'. $drupal_version .'" color="'. $node_connection_color .'" fontcolor="'. $node_connection_color .'"]; '; |
| 605 | |
| 606 | $time_delta[$function_count] = round(($time_index_array[($function_count+1)] - $time_index_array[$function_count])*1000, 3); |
| 607 | $memory_delta[$function_count] = round(($memory_array[($function_count+1)] - $memory_array[$function_count])/1000, 3); |
| 608 | |
| 609 | // Create the node label with the function name, filename, line number & function number, |
| 610 | // as well as the node color and a hypertext link to the specific line number code. |
| 611 | // Procedural Drupal Hooks are labeled with a $via_PHP_function label |
| 612 | // Looping sets of 1, 2 or 3 functions are also labeled with how many times they repeat. |
| 613 | // The label will end up looking something like: |
| 614 | // "require_once__index.php_12_2" [label="require_once\nindex.php\nline: 12\nf2" color="#C7E9B4" href="http://cvs.drupal.org/viewvc.py/drupal/drupal/index.php?annotate=1.91#l12"]; |
| 615 | $node_label = '"'. $function_id .'" [label="'. $function_name .'\n'. $via_filename . $via_PHP_function . $repeating_function_label .'\nline: '. $line_number .'\nf'. $function_count .' -- '. $time_delta[$function_count] .'ms" "'. $level .' "color="'. |
| 616 | $lookup_table[$via_filename]["node_color_hex"] .'" href="'. $lookup_table[$via_filename]["source_code_line_url"] . $line_number .'"];'; |
| 617 | $output_to_dot_all[$function_count] = $node_connection_data . $node_label; |
| 618 | |
| 619 | /** |
| 620 | * If a capture function is passed in via arg(4), then capture the backtrace & forwardtrace data |
| 621 | * (i.e. backtrace = parent functions & forwardtrace = children functions) |
| 622 | */ |
| 623 | if ($capture_function_backtrace_flag) { |
| 624 | if ($capture_contrib_functions) { |
| 625 | // Check to see if this function is called from a contributed module filename |
| 626 | // (i.e. if via_filename begins with 'sites/' -- or whatever the $contrib_module_filepath is) |
| 627 | if (substr($via_filename, 0, strlen($contrib_module_filepath)) == $contrib_module_filepath) { |
| 628 | $capture_contrib_functions_flag = TRUE; |
| 629 | } |
| 630 | } |
| 631 | // Initialize the variable that will flag the capture functions |
| 632 | $capture_function_flag[$function_count] = FALSE; |
| 633 | |
| 634 | // Each time the $capture_function is called, then capture the parent backtrace functions |
| 635 | if ($function_name == $capture_function || $via_filename == $capture_filename || $capture_contrib_functions_flag) { |
| 636 | $function_level_loop = $level; |
| 637 | |
| 638 | // Loop backwards down through the previous function call stack levels |
| 639 | // to extract the function count numbers of all of the functions in the backtrace |
| 640 | while ($function_level_loop > 0) { |
| 641 | // explode the function_id to get the function count |
| 642 | $previous_function_count = explode('__', $function_call_stack[$function_level_loop]); |
| 643 | // Save the *.dot syntax code for that particular function count |
| 644 | // If it is the captured function, then highlight it with a pink box (i.e. a GraphViz subgraph cluster) |
| 645 | // if $capture_function_flag is already TRUE, then the captured function is a child of itself & |
| 646 | // appears in its own backtrace so don't overwrite the label & inserted box |
| 647 | if (!$capture_function_flag[$previous_function_count[3]]) { |
| 648 | // Flag the captured function so that it can be marked with a box later on |
| 649 | // The captured function happens when the function_level_loop = the current $level |
| 650 | // Also keep track of the total number of times that the function occurs |
| 651 | if ($function_level_loop == $level) { |
| 652 | |
| 653 | $subgraph_cluster_count++; |
| 654 | $output_to_dot_function[$previous_function_count[3]] = $output_to_dot_all[$previous_function_count[3]]; |
| 655 | |
| 656 | // Keep track of the captured function's function count to highlight in the callstack table |
| 657 | $capture_function_flag[$previous_function_count[3]] = TRUE; |
| 658 | |
| 659 | // Keep track of the total number of times this function has been called |
| 660 | // including any number of repeats calculated in the visualize_backtrace_detect_repeating_functions() function |
| 661 | if ($function_call_data[$row]['repeating_function_count']) { |
| 662 | $capture_function_count = $capture_function_count + $function_call_data[$row]['repeating_function_count']; |
| 663 | } |
| 664 | // This instance of the capture function is not in a repeating loop |
| 665 | else { |
| 666 | $capture_function_count++; |
| 667 | } |
| 668 | |
| 669 | // Check to see if this is a nested instance of the captured function by first |
| 670 | // checking to see if the $capture_function_children_flag is still set to TRUE |
| 671 | if ($capture_function_children_flag) { |
| 672 | // If the current level is greater than the level stored in the captured function level array, |
| 673 | // then add another level to the depth index. |
| 674 | if ($level > $capture_function_level_array[$capture_function_depth]) { |
| 675 | $capture_function_depth++; |
| 676 | // Add this nested child captured function to the parent capture function's count & time sum |
| 677 | // Start at one less the current depth and go down to the bottom level (i.e. top parent) |
| 678 | $capture_function_depth_key = $capture_function_depth - 1; |
| 679 | while ($capture_function_depth_key >= 1) { |
| 680 | $subgraph_cluster_count_index = $capture_function_cluster_number[$capture_function_depth_key]; |
| 681 | $capture_function_children_time_sum[$subgraph_cluster_count_index] = $capture_function_children_time_sum[$subgraph_cluster_count_index] + $time_delta[$function_count]; |
| 682 | $capture_function_children_count[$subgraph_cluster_count_index]++; |
| 683 | $capture_function_depth_key--; |
| 684 | } |
| 685 | } |
| 686 | // If the depth is greater than one, then first check to see if the depth needs to be reduced, |
| 687 | // and then determine whether or not to add in this function's time to the previous depths (i.e. parent functions) |
| 688 | elseif ($capture_function_depth > 1) { |
| 689 | // Check to see if the depth needs to be iteratively reduced |
| 690 | // (i.e. whenever a nested function's forwardtrace tree ends) |
| 691 | $reduce_depth_flag = TRUE; |
| 692 | while ($reduce_depth_flag) { |
| 693 | // Check to see if the current level is less than or equal to the stored level at the previous depth -- |
| 694 | // while making sure not to decrement the depth down to 0 |
| 695 | if ($level <= $capture_function_level_array[$capture_function_depth-1] && $capture_function_depth >= 1) { |
| 696 | // This marks the end of a previous nested capture function forward trace, |
| 697 | // and so reduce the capture function depth & zero out the stored level array at that depth |
| 698 | $capture_function_level_array[$capture_function_depth] = 0; |
| 699 | $capture_function_depth--; |
| 700 | } |
| 701 | else { |
| 702 | // Stop reducing the depth since this function's $level is now equal to or |
| 703 | // greater than $capture_function_level_array[$capture_function_depth] |
| 704 | $reduce_depth_flag = FALSE; |
| 705 | } |
| 706 | } |
| 707 | // Add this function's time data & count to its parent captured function(s) by looping through the previous depths |
| 708 | $capture_function_depth_key = $capture_function_depth - 1; |
| 709 | while ($capture_function_depth_key >= 1) { |
| 710 | $subgraph_cluster_count_index = $capture_function_cluster_number[$capture_function_depth_key]; |
| 711 | $capture_function_children_time_sum[$subgraph_cluster_count_index] = $capture_function_children_time_sum[$subgraph_cluster_count_index] + $time_delta[$function_count]; |
| 712 | $capture_function_children_count[$subgraph_cluster_count_index]++; |
| 713 | $capture_function_depth_key--; |
| 714 | } |
| 715 | } |
| 716 | } |
| 717 | |
| 718 | // Set the flag to start capturing data on the children functions of this capture function |
| 719 | $capture_function_children_flag = TRUE; |
| 720 | |
| 721 | // Save both the level & count of this captured function in case there are any captured function children |
| 722 | $capture_function_level_array[$capture_function_depth] = $level; |
| 723 | $capture_function_cluster_number[$capture_function_depth] = $subgraph_cluster_count; |
| 724 | |
| 725 | // Determine the number of and elapsed time of all of the functions that come after the captured function -- if any |
| 726 | $capture_function_children_time_sum[$subgraph_cluster_count] = 0; |
| 727 | $capture_function_children_count[$subgraph_cluster_count] = 0; |
| 728 | |
| 729 | // Keep track of where this captured function's function count for inserting the children time sum and count later on |
| 730 | $capture_function_index[$subgraph_cluster_count] = $function_count; |
| 731 | |
| 732 | } |
| 733 | // Save the backtrace nodal connections for this instance of the captured function as it loops backwards through the backtrace levels |
| 734 | else { |
| 735 | $output_to_dot_function[$previous_function_count[3]] = $output_to_dot_all[$previous_function_count[3]]; |
| 736 | } |
| 737 | } |
| 738 | $function_level_loop--; |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | /** |
| 743 | * Now gather the forwardtrace data for the captured function |
| 744 | * Capture the total count and length of time it takes of the children functions of the captured function |
| 745 | * Stop capturing children function data when the current row's $level is equal to or less than the $capture_function_level |
| 746 | * Graph the children functions to the $output_to_dot_function array if arg(3) = 'show_function_forwardtrace' -- |
| 747 | * otherwise the children function count and length of run time is plotted on the backtrace graph |
| 748 | */ |
| 749 | else { |
| 750 | if ($capture_function_children_flag) { |
| 751 | // Check to see if the current level is lower than the last captured function depth |
| 752 | // If so, then either reduce the capture function level depth if it is a nested function |
| 753 | // or stop capturing the child functions of the forwardtraced capture function |
| 754 | if ($level <= $capture_function_level_array[$capture_function_depth]) { |
| 755 | // Check to see if it is at the initial depth, or if the depth needs to be reduced. |
| 756 | if ($capture_function_depth == 1) { |
| 757 | // The current $level is equal to or less than the capture function & is not a nested capture function |
| 758 | // and so stop marking functions as children of the capture function |
| 759 | $capture_function_children_flag = FALSE; |
| 760 | } |
| 761 | else { |
| 762 | // The previous nested capture function tree has ended, and so reduce the depth to the previous level |
| 763 | $reduce_depth_flag = TRUE; |
| 764 | while ($reduce_depth_flag) { |
| 765 | // Check to see if the current level is less than or equal to the stored level at the previous depth -- |
| 766 | // while making sure not to decrement the depth down to 0 |
| 767 | if ($level <= $capture_function_level_array[$capture_function_depth-1] && $capture_function_depth >= 1) { |
| 768 | // This marks the end of a previous nested capture function forward trace, |
| 769 | // and so reduce the capture function depth & zero out the stored level array at that depth |
| 770 | $capture_function_level_array[$capture_function_depth] = 0; |
| 771 | $capture_function_depth--; |
| 772 | } |
| 773 | else { |
| 774 | // Stop reducing the depth because this function's $level is now equal to or |
| 775 | // greater than $capture_function_level_array[$capture_function_depth] |
| 776 | $reduce_depth_flag = FALSE; |
| 777 | } |
| 778 | } |
| 779 | // Do a final check after reducing the depth to see if the $level is equal to or less than |
| 780 | // the stored level @ capture_function_depth=1 |
| 781 | if ($level <= $capture_function_level_array[$capture_function_depth]) { |
| 782 | // Stop marking functions as children of the capture function |
| 783 | $capture_function_children_flag = FALSE; |
| 784 | } |
| 785 | } |
| 786 | } |
| 787 | |
| 788 | // Store the child data for this $capture_function |
| 789 | if ($capture_function_children_flag) { |
| 790 | // Save the children functions to the *.dot file if the Forwardtrace flag is set |
| 791 | if ($capture_function_forwardtrace_flag) { |
| 792 | $output_to_dot_function[$function_count] = $output_to_dot_all[$function_count]; |
| 793 | } |
| 794 | // Sum the total length of all of the children functions of the capture function |
| 795 | // Start at the lowest depth and loop through up to the current depth |
| 796 | $capture_function_depth_key = 1; |
| 797 | while ($capture_function_depth_key <= $capture_function_depth) { |
| 798 | $subgraph_cluster_count_index = $capture_function_cluster_number[$capture_function_depth_key]; |
| 799 | $capture_function_children_time_sum[$subgraph_cluster_count_index] = $capture_function_children_time_sum[$subgraph_cluster_count_index] + $time_delta[$function_count]; |
| 800 | $capture_function_children_count[$subgraph_cluster_count_index]++; |
| 801 | $capture_function_depth_key++; |
| 802 | } |
| 803 | } |
| 804 | } |
| 805 | } |
| 806 | $capture_contrib_functions_flag = FALSE; |
| 807 | } // END of capturing the backtrace parent functions & forwardtrace children functions |
| 808 | |
| 809 | // Capture all of the functions in between {main} and menu_execute_active_handler @ level=2 |
| 810 | // This contains the Drupal bootstrap section which doesn't have a lot of variations |
| 811 | if ($function_name == "{main}") { |
| 812 | $section_endpoint[1]['begin'] = $function_count; |
| 813 | } |
| 814 | // Capture all of the functions in between menu_execute_active_handler @ level=3 and theme @ level=2 |
| 815 | // This section is usually the bulk of the relevent functions and may need to be broken up in order to be plotted all at once |
| 816 | // Section two is broken up into three equal sections in section numbers 4, 5 & 6 |
| 817 | if ($function_name == "menu_execute_active_handler" && $level == 2) { |
| 818 | $section_endpoint[1]['end'] = $function_count-1; |
| 819 | $section_endpoint[2]['begin'] = $function_count; |
| 820 | } |
| 821 | // Capture all of the functions in between theme @ level=2 and the end |
| 822 | // This contains a lot of the theming-specific code. |
| 823 | if ($function_name == "theme" && $level == 2) { |
| 824 | $index_theme_executed = TRUE; |
| 825 | $section_endpoint[2]['end'] = $function_count-1; |
| 826 | $section_endpoint[3]['begin'] = $function_count; |
| 827 | } |
| 828 | } |
| 829 | $row++; |
| 830 | } |
| 831 | // Determine the final $row number for the last subsection |
| 832 | if ($index_theme_executed) { |
| 833 | $section_endpoint[3]['end'] = $function_count; |
| 834 | } |
| 835 | else { |
| 836 | $section_endpoint[2]['end'] = $function_count; |
| 837 | } |
| 838 | |
| 839 | // Sometimes the Main Section is too big to be plotted all at once, and so split it up into three equal sections |
| 840 | // Creating three main subsecions is an option that can be turned off in the admin section |
| 841 | $create_main_subsections = variable_get('visualize_backtrace_create_main_subsections', 1); |
| 842 | if ($create_main_subsections) { |
| 843 | $section_endpoint[4]['begin'] = $section_endpoint[2]['begin']; |
| 844 | $section_endpoint[4]['end'] = round(($section_endpoint[2]['end']-$section_endpoint[2]['begin'])/3) + $section_endpoint[4]['begin']; |
| 845 | $section_endpoint[5]['begin'] = $section_endpoint[4]['end']; |
| 846 | $section_endpoint[5]['end'] = round(($section_endpoint[2]['end']-$section_endpoint[2]['begin'])/3) + $section_endpoint[5]['begin']; |
| 847 | $section_endpoint[6]['begin'] = $section_endpoint[5]['end']; |
| 848 | $section_endpoint[6]['end'] = $section_endpoint[2]['end']; |
| 849 | $section = 6; |
| 850 | $main_sections = 6; |
| 851 | } |
| 852 | else { |
| 853 | $section = 3; |
| 854 | $main_sections = 3; |
| 855 | } |
| 856 | |
| 857 | //// DEBUGGING: Print the start and stop row numbers for each of the 6 sections |
| 858 | //$i=1; |
| 859 | //while ($i <= $section) { |
| 860 | // print $i ." begin = ". $section_endpoint[$i]['begin']. " end = ". $section_endpoint[$i]['end'] ."<br>"; |
| 861 | // $i++; |
| 862 | //} |
| 863 | |
| 864 | // Loop through each of the section numbers |
| 865 | $section_number = 0; |
| 866 | $output_to_dot = array(); |
| 867 | |
| 868 | // Section 0 contains all of the all three subsections |
| 869 | $output_to_dot[0] = 'digraph G {graph [ordering="out"]; node [style=filled];'. implode("", $output_to_dot_all) .'}'; |
| 870 | $section_length[0] = count($output_to_dot_all); |
| 871 | |
| 872 | |
| 873 | // Add another section for the captured backtrace for a specific function if the |
| 874 | // arg(3) is either 'show_function_backtrace' or 'show_function_forwardtrace' |
| 875 | if ($capture_function_backtrace_flag) { |
| 876 | // Insert a box around the captured functions that includes the total number and length of child functions of each captured function |
| 877 | // The box is a "subgraph cluster" that is inserted after the nodal connection & before the node label |
| 878 | // This is done by replacing '"]; "' with the subgraph code of '"]; subgraph cluster1{color="#000000"; label="Function #01 w/ 8 children = 1.068ms" "' |
| 879 | foreach ($capture_function_index as $subcluster_count_key => $function_count_value) { |
| 880 | if ($capture_function_children_count[$subcluster_count_key] > 0) { |
| 881 | if ($capture_function_children_time_sum[$subcluster_count_key] > $time_threshold) { |
| 882 | // Place a red box around the functions that exceed the time threshold (default threshold is 5 ms) |
| 883 | $box_color = "#FF9BD1"; |
| 884 | $add_mark = TRUE; |
| 885 | } |
| 886 | else { |
| 887 | // Place a green box around all of the other functions that are below the time threshold |
| 888 | $box_color = "#7FCDBB"; |
| 889 | $add_mark = FALSE; |
| 890 | } |
| 891 | // Place a leading 0 before the fuction numbers less than 10 |
| 892 | if ($subcluster_count_key < 10) { |
| 893 | // Re-save this capture function child data w/ the function count as the array index to display in the callstack table |
| 894 | $capture_function_child_data[$function_count_value] = 'Function #0'. $subcluster_count_key .' w/ '. $capture_function_children_count[$subcluster_count_key] .' children = '. $capture_function_children_time_sum[$subcluster_count_key] .'ms'; |
| 895 | } |
| 896 | else { |
| 897 | // Re-save this capture function child data w/ the function count as the array index to display in the callstack table |
| 898 | $capture_function_child_data[$function_count_value] = 'Function #'. $subcluster_count_key .' w/ '. $capture_function_children_count[$subcluster_count_key] .' children = '. $capture_function_children_time_sum[$subcluster_count_key] .'ms'; |
| 899 | } |
| 900 | } |
| 901 | // Add a green box around the functions that have no child functions |
| 902 | else { |
| 903 | $box_color = "#7FCDBB"; |
| 904 | $add_mark = FALSE; |
| 905 | if ($subcluster_count_key < 10) { |
| 906 | $capture_function_child_data[$function_count_value] = 'Function #0'. $subcluster_count_key .' w/ 0 children'; |
| 907 | } |
| 908 | else { |
| 909 | $capture_function_child_data[$function_count_value] = 'Function #'. $subcluster_count_key .' w/ 0 children'; |
| 910 | } |
| 911 | } |
| 912 | $insert_string = '"]; subgraph cluster'. $subcluster_count_key .'{color="'. $box_color .'"; label="'. $capture_function_child_data[$function_count_value] .'" "'; |
| 913 | $output_to_dot_function[$function_count_value] = preg_replace('/"]; "/', $insert_string, $output_to_dot_function[$function_count_value]) .'}'; |
| 914 | // Highlight this data red when displayed in the table |
| 915 | if ($add_mark) { |
| 916 | $capture_function_child_data[$function_count_value] = '<span class="marker">'. $capture_function_child_data[$function_count_value] .'</span>'; |
| 917 | } |
| 918 | // Calculate the average time of each captured function + it's children and divide by the total number of captured function clusters at the end |
| 919 | $capture_function_children_time_average = $capture_function_children_time_sum[$subcluster_count_key] + $time_delta[$function_count_value] + $capture_function_children_time_average; |
| 920 | } |
| 921 | $capture_function_children_time_total = $capture_function_children_time_average; |
| 922 | $capture_function_children_time_average = round($capture_function_children_time_average / $subgraph_cluster_count, 3); |
| 923 | |
| 924 | // If there are three main subsections, then create a total of 7 *.dot sections. |
| 925 | if ($create_main_subsections) { |
| 926 | $section = 7; |
| 927 | } |
| 928 | else { |
| 929 | $section = 4; |
| 930 | } |
| 931 | $output_to_dot[$section] = 'digraph G {graph [ordering="out"]; node [style=filled];'. implode("", $output_to_dot_function) .'}'; |
| 932 | } |
| 933 | |
| 934 | $zgrviewer_location = variable_get('visualize_backtrace_zgrviewer_location', '/usr/local/bin/zgrviewer'); |
| 935 | |
| 936 | // Create the *.dot file for each subsection |
| 937 | while ($section_number <= $section) { |
| 938 | // The *.dot files for the six main sections are created by taking an array_slice of output_to_dot_all |
| 939 | // And the *.dot file for the captured function is created from the output_to_dot_function |
| 940 | if ($section_number > 0 && $section_number <= $main_sections) { |
| 941 | // Calculate how many function calls were made in each section |
| 942 | $section_length[$section_number] = $section_endpoint[$section_number]['end']-$section_endpoint[$section_number]['begin']; |
| 943 | |
| 944 | // Generate the *.dot file for each of three sub-sections |
| 945 | $output_to_dot[$section_number] = 'digraph G {graph [ordering="out"]; node [style=filled];'. |
| 946 | implode("", array_slice($output_to_dot_all, $section_endpoint[$section_number]['begin']-1, $section_length[$section_number]+1)) |
| 947 | .'}'; |
| 948 | } |
| 949 | |
| 950 | // There is an extra space at the end of $unix_base_path that needs to be deleted. |
| 951 | $default_file_path = file_directory_path(); |
| 952 | $dot_file[$section_number] = trim($unix_base_path) ."/". $default_file_path ."/". $trace_number ."_section". $section_number .".dot"; |
| 953 | $output_file = fopen($dot_file[$section_number], 'w'); |
| 954 | fwrite($output_file, $output_to_dot[$section_number]); |
| 955 | fclose($output_file); |
| 956 | $command[$section_number] = $zgrviewer_location ."/run.sh --Pdot ". $dot_file[$section_number] ."; "; |
| 957 | $section_number++; |
| 958 | } |
| 959 | |
| 960 | // ** OUTPUT THE LINKS TO THE GRAPHS & OUTPUT THE DATA ** |
| 961 | // Clear out some of the large arrays |
| 962 | $output_to_dot = array(); |
| 963 | $output_to_dot_all = array(); |
| 964 | |
| 965 | $add_footnote = FALSE; |
| 966 | |
| 967 | // Get the file sizes by piping in "ls -l" into awk and extracting the fifth column |
| 968 | // ls -l /Library/WebServer/Documents/d52/files/1189710511* | awk '{print $5}' |
| 969 | $get_file_sizes_command = "ls -l ". trim($unix_base_path) ."/files/". $trace_number ."* | awk '{print $5}'"; |
| 970 | exec($get_file_sizes_command, $files_output); |
| 971 | |
| 972 | // Add a note to any file sizes that are greater than 1000000 bytes |
| 973 | foreach ($files_output as $section_key => $filesize_value) { |
| 974 | if ($filesize_value > 1000000) { |
| 975 | $files_output[$section_key] = $files_output[$section_key] .' bytes [*]'; |
| 976 | $add_footnote = TRUE; |
| 977 | } |
| 978 | else { |
| 979 | $files_output[$section_key] = $files_output[$section_key] .' bytes'; |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | // Indicate where the trace file originated if it was called directly from the page load |
| 984 | // Don't display the HTTP reference if the previous a page had visualize_backtrace as arg(0) |
| 985 | // Determine which array position arg(0) is located in the exploded HTTP_REFERER array |
| 986 | // by counting the length of the exploded $base_url |
| 987 | $http_reference = explode('/', $_SERVER['HTTP_REFERER']); |
| 988 | if ($http_reference[count(explode('/', $base_url))] != "visualize_backtrace") { |
| 989 | $output = '<br />Graph of '. l($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_REFERER']) .'<br />'; |
| 990 | } |
| 991 | else { |
| 992 | $output = ''; |
| 993 | } |
| 994 | |
| 995 | $output .= t('The %trace_file page load executed in %final_time milleseconds<br />', array('%trace_file' => $trace_file, '%final_time' => round($final_time * 1000, 3))); // DELETED FOR NOW: with a memory footprint of '. round($final_memory / 1000, 3) .' kB. |
| 996 | // $output .= 'The page load for this trace executed in <em>'. round($final_time * 1000, 3) .'</em> milleseconds<br />'; // DELETED FOR NOW: with a memory footprint of '. round($final_memory / 1000, 3) .' kB. |
| 997 | $output .= t('%total_rows Total Drupal Functions appearing in %section_length clusters after consolidating the repeating function loops.', array('%total_rows' => $total_rows, '%section_length' => $section_length[0])) .'<br />'; |
| 998 | |
| 999 | $output .= '<br /><h3>Backtrace Graphs</h3>'; |
| 1000 | // TODO: Possibly replace this hardcoded table with the Drupal table theming as in devel_query_table($queries, $counts); |
| 1001 | $output .= '<table>'; |
| 1002 | $output .= '<tr><td>0.) Full Backtrace</td><td>'. $section_length[0] .' functions</td><td>'. $files_output[0] .' </td><td>'. l('Graph Full Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_0') .'</td></tr>'; |
| 1003 | // Indent each table to make the parent/child relationships clear |
| 1004 | // The section lengths calculated above were for the correct partitioning of the $output_to_dot_all data |
| 1005 | // through the array_slice & implosion when creating the *.dot files, and so add 1 to get the correct section lengths |
| 1006 | $output .= '<tr><td> 1.) Drupal Bootstrap</td><td> '. ($section_length[1] + 1) .' functions</td><td> '. $files_output[1] .' </td><td> '. l('Graph Bootstrap Section Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_1') .'</td></tr>'; |
| 1007 | $output .= '<tr><td> 2.) Main Section</td><td> '. ($section_length[2] + 1) .' functions</td><td> '. $files_output[2] .' </td><td> '. l('Graph Main Section Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_2') .'</td></tr>'; |
| 1008 | if ($create_main_subsections) { |
| 1009 | $output .= '<tr><td> 4.) Main (Part 1/3)</td><td> '. $section_length[4] .' functions</td><td> '. $files_output[4] .' </td><td> '. l('Graph Main Section 1', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_4') .'</td></tr>'; |
| 1010 | $output .= '<tr><td> 5.) Main (Part 2/3)</td><td> '. $section_length[5] .' functions</td><td> '. $files_output[5] .' </td><td> '. l('Graph Main Section 2', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_5') .'</td></tr>'; |
| 1011 | $output .= '<tr><td> 6.) Main (Part 3/3)</td><td> '. ($section_length[6] + 1) .' functions</td><td> '. $files_output[6] .' </td><td> '. l('Graph Main Section 3', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_6') .'</td></tr>'; |
| 1012 | } |
| 1013 | // Check to see if the theme() function was even called from index.php. If the section length is 0, then don't output a link to it. |
| 1014 | if ($section_length[3] > 0) { |
| 1015 | $output .= '<tr><td> 3.) Theming</td><td> '. ($section_length[3] + 1) .' functions</td><td> '. $files_output[3] .' </td><td> '. l('Graph Theme Section Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/section_3') .'</td></tr>'; |
| 1016 | } |
| 1017 | $output .= '</table>'; |
| 1018 | |
| 1019 | // Add a footnote if if there are any files larger than 1000000 bytes |
| 1020 | if ($add_footnote) $output .= '<em>[*] - Indicates a large file size, which either takes longer to plot or doesn\'t plot at all.<br /><br /></em>'; |
| 1021 | |
| 1022 | // If any function or filename backtraces were captured, then publish another header with more details and links |
| 1023 | if ($capture_function_backtrace_flag) { |
| 1024 | if ($capture_function_forwardtrace_flag) { |
| 1025 | $add_text = ' and Forwardtraces'; |
| 1026 | } |
| 1027 | // Create the header information for when a specific function is passed in via arg(4) |
| 1028 | if ($subsection_argument == "show_function_backtrace" || $subsection_argument == "show_function_forwardtrace") { |
| 1029 | $output .= '<h3>Backtraces'. $add_text .' for the Function "'. $capture_function .'"</h3>'; |
| 1030 | // TODO: Possibly replace this hardcoded table with the Drupal table theming as in devel_query_table($queries, $counts); |
| 1031 | $output .= '<table><tr><td>'. $section .'.) "'. $capture_function .'"</td><td>'. count($output_to_dot_function) .' total functions</td><td>'. $files_output[7] .' </td><td>'. |
| 1032 | l('Graph Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_function_backtrace/'. $capture_function) .'<br />'. |
| 1033 | l('Graph Forwardtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_function_forwardtrace/'. $capture_function) .'</td></tr></table>'; |
| 1034 | } |
| 1035 | else { |
| 1036 | // Create the header information for when all of the contributed module functions are being captured |
| 1037 | // (i.e. arg(4) = "capture_all_contrib_functions") |
| 1038 | if ($capture_filename == 'capture_all_contrib_functions') { |
| 1039 | if ($contrib_module_filepath == 'sites/') { |
| 1040 | $argument_label = "All Contributed Module Functions"; |
| 1041 | } |
| 1042 | else { |
| 1043 | $argument_label = 'Functions from '. $contrib_module_filepath; |
| 1044 | } |
| 1045 | |
| 1046 | $output .= '<h3>Backtraces'. $add_text .' of '. $argument_label .'</h3>'; |
| 1047 | // TODO: Possibly replace this hardcoded table with the Drupal table theming as in devel_query_table($queries, $counts); |
| 1048 | $output .= '<table><tr><td>'. $section .'.) '. $argument_label .'</td><td>'. count($output_to_dot_function) .' total functions</td><td>'. $files_output[7] .' </td><td>'. |
| 1049 | l('Graph Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_filename_backtrace/capture_all_contrib_functions') .'<br />'. |
| 1050 | l('Graph Forwardtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_filename_forwardtrace/capture_all_contrib_functions') .'</td></tr></table>'; |
| 1051 | } |
| 1052 | // Otherwise print out the header information for when a specific filename is being captured |
| 1053 | else { |
| 1054 | // Replace the filepath '/' with '-' to create one URL argument |
| 1055 | $graph_filename = preg_replace('/\//', '-', $capture_filename); |
| 1056 | $output .= '<h3>Backtraces'. $add_text .' of All Functions from the Filename "'. $capture_filename .'"</h3>'; |
| 1057 | // TODO: Possibly replace this hardcoded table with the Drupal table theming as in devel_query_table($queries, $counts); |
| 1058 | $output .= '<table><tr><td>'. $section .'.) "'. $capture_filename .'"</td><td>'. count($output_to_dot_function) .' total functions</td><td>'. $files_output[7] .' </td><td>'. |
| 1059 | l('Graph Backtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_filename_backtrace/'. $graph_filename) .'<br />'. |
| 1060 | l('Graph Forwardtrace', 'visualize_backtrace/'. $trace_number .'/view_callstack/show_filename_forwardtrace/'. $graph_filename) .'</td></tr></table>'; |
| 1061 | } |
| 1062 | } |
| 1063 | } |
| 1064 | |
| 1065 | // Print out the all of the function call stack data |
| 1066 | $output .= '<h3>Function Call Stack</h3>'; |
| 1067 | |
| 1068 | // Determine if arg(3) passes in a specific function number (e.g. 's |