2014-02-10 02:10:30 +01:00
/*************************************************************************/
2020-02-27 03:30:20 +01:00
/* local_debugger.cpp */
2014-02-10 02:10:30 +01:00
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
2017-08-27 14:16:55 +02:00
/* https://godotengine.org */
2014-02-10 02:10:30 +01:00
/*************************************************************************/
2022-01-03 21:27:34 +01:00
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
2014-02-10 02:10:30 +01:00
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
2018-01-05 00:50:27 +01:00
2020-02-27 03:30:20 +01:00
# include "local_debugger.h"
2017-01-16 08:04:19 +01:00
2020-02-27 03:30:20 +01:00
# include "core/debugger/script_debugger.h"
2022-06-30 03:45:37 +02:00
# include "core/os/os.h"
2014-02-10 02:10:30 +01:00
2020-02-27 03:30:20 +01:00
struct LocalDebugger : : ScriptsProfiler {
struct ProfileInfoSort {
bool operator ( ) ( const ScriptLanguage : : ProfilingInfo & A , const ScriptLanguage : : ProfilingInfo & B ) const {
return A . total_time > B . total_time ;
}
} ;
2021-02-02 03:16:37 +01:00
double frame_time = 0 ;
2020-02-27 03:30:20 +01:00
uint64_t idle_accum = 0 ;
Vector < ScriptLanguage : : ProfilingInfo > pinfo ;
void toggle ( bool p_enable , const Array & p_opts ) {
if ( p_enable ) {
for ( int i = 0 ; i < ScriptServer : : get_language_count ( ) ; i + + ) {
ScriptServer : : get_language ( i ) - > profiling_start ( ) ;
}
print_line ( " BEGIN PROFILING " ) ;
pinfo . resize ( 32768 ) ;
} else {
_print_frame_data ( true ) ;
for ( int i = 0 ; i < ScriptServer : : get_language_count ( ) ; i + + ) {
ScriptServer : : get_language ( i ) - > profiling_stop ( ) ;
}
}
}
2022-05-05 20:23:47 +02:00
void tick ( double p_frame_time , double p_process_time , double p_physics_time , double p_physics_frame_time ) {
2020-02-27 03:30:20 +01:00
frame_time = p_frame_time ;
_print_frame_data ( false ) ;
}
void _print_frame_data ( bool p_accumulated ) {
uint64_t diff = OS : : get_singleton ( ) - > get_ticks_usec ( ) - idle_accum ;
2020-05-14 16:41:43 +02:00
if ( ! p_accumulated & & diff < 1000000 ) { //show every one second
2020-02-27 03:30:20 +01:00
return ;
2020-05-14 16:41:43 +02:00
}
2020-02-27 03:30:20 +01:00
idle_accum = OS : : get_singleton ( ) - > get_ticks_usec ( ) ;
int ofs = 0 ;
for ( int i = 0 ; i < ScriptServer : : get_language_count ( ) ; i + + ) {
2020-05-14 16:41:43 +02:00
if ( p_accumulated ) {
2020-02-27 03:30:20 +01:00
ofs + = ScriptServer : : get_language ( i ) - > profiling_get_accumulated_data ( & pinfo . write [ ofs ] , pinfo . size ( ) - ofs ) ;
2020-05-14 16:41:43 +02:00
} else {
2020-02-27 03:30:20 +01:00
ofs + = ScriptServer : : get_language ( i ) - > profiling_get_frame_data ( & pinfo . write [ ofs ] , pinfo . size ( ) - ofs ) ;
2020-05-14 16:41:43 +02:00
}
2020-02-27 03:30:20 +01:00
}
SortArray < ScriptLanguage : : ProfilingInfo , ProfileInfoSort > sort ;
sort . sort ( pinfo . ptrw ( ) , ofs ) ;
// compute total script frame time
uint64_t script_time_us = 0 ;
for ( int i = 0 ; i < ofs ; i + + ) {
script_time_us + = pinfo [ i ] . self_time ;
}
2021-02-02 03:16:37 +01:00
double script_time = USEC_TO_SEC ( script_time_us ) ;
double total_time = p_accumulated ? script_time : frame_time ;
2020-02-27 03:30:20 +01:00
if ( ! p_accumulated ) {
print_line ( " FRAME: total: " + rtos ( total_time ) + " script: " + rtos ( script_time ) + " / " + itos ( script_time * 100 / total_time ) + " % " ) ;
} else {
print_line ( " ACCUMULATED: total: " + rtos ( total_time ) ) ;
}
for ( int i = 0 ; i < ofs ; i + + ) {
print_line ( itos ( i ) + " : " + pinfo [ i ] . signature ) ;
2021-02-02 03:16:37 +01:00
double tt = USEC_TO_SEC ( pinfo [ i ] . total_time ) ;
double st = USEC_TO_SEC ( pinfo [ i ] . self_time ) ;
2020-02-27 03:30:20 +01:00
print_line ( " \t total: " + rtos ( tt ) + " / " + itos ( tt * 100 / total_time ) + " % \t self: " + rtos ( st ) + " / " + itos ( st * 100 / total_time ) + " % tcalls: " + itos ( pinfo [ i ] . call_count ) ) ;
}
}
ScriptsProfiler ( ) {
idle_accum = OS : : get_singleton ( ) - > get_ticks_usec ( ) ;
}
} ;
void LocalDebugger : : debug ( bool p_can_continue , bool p_is_error_breakpoint ) {
ScriptLanguage * script_lang = script_debugger - > get_break_language ( ) ;
2014-02-10 02:10:30 +01:00
2020-12-15 13:04:21 +01:00
if ( ! target_function . is_empty ( ) ) {
2020-02-27 03:30:20 +01:00
String current_function = script_lang - > debug_get_stack_level_function ( 0 ) ;
2018-02-11 12:08:37 +01:00
if ( current_function ! = target_function ) {
2020-02-27 03:30:20 +01:00
script_debugger - > set_depth ( 0 ) ;
script_debugger - > set_lines_left ( 1 ) ;
2018-02-11 12:08:37 +01:00
return ;
}
target_function = " " ;
}
2020-02-27 03:30:20 +01:00
print_line ( " \n Debugger Break, Reason: ' " + script_lang - > debug_get_error ( ) + " ' " ) ;
print_line ( " *Frame " + itos ( 0 ) + " - " + script_lang - > debug_get_stack_level_source ( 0 ) + " : " + itos ( script_lang - > debug_get_stack_level_line ( 0 ) ) + " in function ' " + script_lang - > debug_get_stack_level_function ( 0 ) + " ' " ) ;
2014-02-10 02:10:30 +01:00
print_line ( " Enter \" help \" for assistance. " ) ;
int current_frame = 0 ;
2020-02-27 03:30:20 +01:00
int total_frames = script_lang - > debug_get_stack_level_count ( ) ;
2014-02-10 02:10:30 +01:00
while ( true ) {
OS : : get_singleton ( ) - > print ( " debug> " ) ;
String line = OS : : get_singleton ( ) - > get_stdin_string ( ) . strip_edges ( ) ;
2018-02-11 12:08:37 +01:00
// Cache options
String variable_prefix = options [ " variable_prefix " ] ;
2021-12-09 10:42:46 +01:00
if ( line . is_empty ( ) ) {
2020-02-27 03:30:20 +01:00
print_line ( " \n Debugger Break, Reason: ' " + script_lang - > debug_get_error ( ) + " ' " ) ;
print_line ( " *Frame " + itos ( current_frame ) + " - " + script_lang - > debug_get_stack_level_source ( current_frame ) + " : " + itos ( script_lang - > debug_get_stack_level_line ( current_frame ) ) + " in function ' " + script_lang - > debug_get_stack_level_function ( current_frame ) + " ' " ) ;
2014-02-10 02:10:30 +01:00
print_line ( " Enter \" help \" for assistance. " ) ;
2020-05-14 16:41:43 +02:00
} else if ( line = = " c " | | line = = " continue " ) {
2014-02-10 02:10:30 +01:00
break ;
2020-05-14 16:41:43 +02:00
} else if ( line = = " bt " | | line = = " breakpoint " ) {
2014-02-10 02:10:30 +01:00
for ( int i = 0 ; i < total_frames ; i + + ) {
String cfi = ( current_frame = = i ) ? " * " : " " ; //current frame indicator
2020-02-27 03:30:20 +01:00
print_line ( cfi + " Frame " + itos ( i ) + " - " + script_lang - > debug_get_stack_level_source ( i ) + " : " + itos ( script_lang - > debug_get_stack_level_line ( i ) ) + " in function ' " + script_lang - > debug_get_stack_level_function ( i ) + " ' " ) ;
2014-02-10 02:10:30 +01:00
}
} else if ( line . begins_with ( " fr " ) | | line . begins_with ( " frame " ) ) {
if ( line . get_slice_count ( " " ) = = 1 ) {
2020-02-27 03:30:20 +01:00
print_line ( " *Frame " + itos ( current_frame ) + " - " + script_lang - > debug_get_stack_level_source ( current_frame ) + " : " + itos ( script_lang - > debug_get_stack_level_line ( current_frame ) ) + " in function ' " + script_lang - > debug_get_stack_level_function ( current_frame ) + " ' " ) ;
2014-02-10 02:10:30 +01:00
} else {
2015-06-29 05:29:49 +02:00
int frame = line . get_slicec ( ' ' , 1 ) . to_int ( ) ;
2014-02-10 02:10:30 +01:00
if ( frame < 0 | | frame > = total_frames ) {
print_line ( " Error: Invalid frame. " ) ;
} else {
current_frame = frame ;
2020-02-27 03:30:20 +01:00
print_line ( " *Frame " + itos ( frame ) + " - " + script_lang - > debug_get_stack_level_source ( frame ) + " : " + itos ( script_lang - > debug_get_stack_level_line ( frame ) ) + " in function ' " + script_lang - > debug_get_stack_level_function ( frame ) + " ' " ) ;
2014-02-10 02:10:30 +01:00
}
}
2018-02-11 12:08:37 +01:00
} else if ( line . begins_with ( " set " ) ) {
if ( line . get_slice_count ( " " ) = = 1 ) {
2021-08-09 22:13:42 +02:00
for ( const KeyValue < String , String > & E : options ) {
print_line ( " \t " + E . key + " = " + E . value ) ;
2018-02-11 12:08:37 +01:00
}
} else {
String key_value = line . get_slicec ( ' ' , 1 ) ;
int value_pos = key_value . find ( " = " ) ;
if ( value_pos < 0 ) {
print_line ( " Error: Invalid set format. Use: set key=value " ) ;
} else {
String key = key_value . left ( value_pos ) ;
if ( ! options . has ( key ) ) {
print_line ( " Error: Unknown option " + key ) ;
} else {
// Allow explicit tab character
2020-02-13 16:42:49 +01:00
String value = key_value . substr ( value_pos + 1 ) . replace ( " \\ t " , " \t " ) ;
2018-02-11 12:08:37 +01:00
options [ key ] = value ;
}
}
}
2014-02-10 02:10:30 +01:00
} else if ( line = = " lv " | | line = = " locals " ) {
List < String > locals ;
List < Variant > values ;
2020-02-27 03:30:20 +01:00
script_lang - > debug_get_stack_level_locals ( current_frame , & locals , & values ) ;
2018-02-11 12:08:37 +01:00
print_variables ( locals , values , variable_prefix ) ;
2014-02-10 02:10:30 +01:00
} else if ( line = = " gv " | | line = = " globals " ) {
2018-02-11 12:08:37 +01:00
List < String > globals ;
2014-02-10 02:10:30 +01:00
List < Variant > values ;
2020-02-27 03:30:20 +01:00
script_lang - > debug_get_globals ( & globals , & values ) ;
2018-02-11 12:08:37 +01:00
print_variables ( globals , values , variable_prefix ) ;
2014-02-10 02:10:30 +01:00
} else if ( line = = " mv " | | line = = " members " ) {
2018-02-11 12:08:37 +01:00
List < String > members ;
2014-02-10 02:10:30 +01:00
List < Variant > values ;
2020-02-27 03:30:20 +01:00
script_lang - > debug_get_stack_level_members ( current_frame , & members , & values ) ;
2018-02-11 12:08:37 +01:00
print_variables ( members , values , variable_prefix ) ;
2014-02-10 02:10:30 +01:00
} else if ( line . begins_with ( " p " ) | | line . begins_with ( " print " ) ) {
if ( line . get_slice_count ( " " ) < = 1 ) {
print_line ( " Usage: print <expre> " ) ;
} else {
2015-06-29 05:29:49 +02:00
String expr = line . get_slicec ( ' ' , 2 ) ;
2020-02-27 03:30:20 +01:00
String res = script_lang - > debug_parse_stack_level_expression ( current_frame , expr ) ;
2014-02-10 02:10:30 +01:00
print_line ( res ) ;
}
} else if ( line = = " s " | | line = = " step " ) {
2020-02-27 03:30:20 +01:00
script_debugger - > set_depth ( - 1 ) ;
script_debugger - > set_lines_left ( 1 ) ;
2014-02-10 02:10:30 +01:00
break ;
2018-02-11 12:08:37 +01:00
} else if ( line = = " n " | | line = = " next " ) {
2020-02-27 03:30:20 +01:00
script_debugger - > set_depth ( 0 ) ;
script_debugger - > set_lines_left ( 1 ) ;
2014-02-10 02:10:30 +01:00
break ;
2018-02-11 12:08:37 +01:00
} else if ( line = = " fin " | | line = = " finish " ) {
2020-02-27 03:30:20 +01:00
String current_function = script_lang - > debug_get_stack_level_function ( 0 ) ;
2018-02-11 12:08:37 +01:00
for ( int i = 0 ; i < total_frames ; i + + ) {
2020-02-27 03:30:20 +01:00
target_function = script_lang - > debug_get_stack_level_function ( i ) ;
2018-02-11 12:08:37 +01:00
if ( target_function ! = current_function ) {
2020-02-27 03:30:20 +01:00
script_debugger - > set_depth ( 0 ) ;
script_debugger - > set_lines_left ( 1 ) ;
2018-02-11 12:08:37 +01:00
return ;
}
}
print_line ( " Error: Reached last frame. " ) ;
target_function = " " ;
2014-02-10 02:10:30 +01:00
} else if ( line . begins_with ( " br " ) | | line . begins_with ( " break " ) ) {
if ( line . get_slice_count ( " " ) < = 1 ) {
2022-05-19 17:00:06 +02:00
const HashMap < int , HashSet < StringName > > & breakpoints = script_debugger - > get_breakpoints ( ) ;
2018-02-11 12:08:37 +01:00
if ( breakpoints . size ( ) = = 0 ) {
print_line ( " No Breakpoints. " ) ;
continue ;
}
print_line ( " Breakpoint(s): " + itos ( breakpoints . size ( ) ) ) ;
2022-05-19 17:00:06 +02:00
for ( const KeyValue < int , HashSet < StringName > > & E : breakpoints ) {
print_line ( " \t " + String ( * E . value . begin ( ) ) + " : " + itos ( E . key ) ) ;
2018-02-11 12:08:37 +01:00
}
2014-02-10 02:10:30 +01:00
} else {
2018-02-11 12:08:37 +01:00
Pair < String , int > breakpoint = to_breakpoint ( line ) ;
String source = breakpoint . first ;
int linenr = breakpoint . second ;
2014-02-10 02:10:30 +01:00
2020-12-15 13:04:21 +01:00
if ( source . is_empty ( ) ) {
2018-02-11 12:08:37 +01:00
continue ;
2020-05-14 16:41:43 +02:00
}
2014-02-10 02:10:30 +01:00
2020-02-27 03:30:20 +01:00
script_debugger - > insert_breakpoint ( linenr , source ) ;
2014-02-10 02:10:30 +01:00
2018-02-11 12:08:37 +01:00
print_line ( " Added breakpoint at " + source + " : " + itos ( linenr ) ) ;
2014-02-10 02:10:30 +01:00
}
2018-02-11 12:08:37 +01:00
} else if ( line = = " q " | | line = = " quit " ) {
// Do not stop again on quit
2020-02-27 03:30:20 +01:00
script_debugger - > clear_breakpoints ( ) ;
script_debugger - > set_depth ( - 1 ) ;
script_debugger - > set_lines_left ( - 1 ) ;
2018-02-11 12:08:37 +01:00
2022-06-30 03:45:37 +02:00
MainLoop * main_loop = OS : : get_singleton ( ) - > get_main_loop ( ) ;
if ( main_loop - > get_class ( ) = = " SceneTree " ) {
main_loop - > call ( " quit " ) ;
}
2018-02-11 12:08:37 +01:00
break ;
2014-02-10 02:10:30 +01:00
} else if ( line . begins_with ( " delete " ) ) {
if ( line . get_slice_count ( " " ) < = 1 ) {
2020-02-27 03:30:20 +01:00
script_debugger - > clear_breakpoints ( ) ;
2014-02-10 02:10:30 +01:00
} else {
2018-02-11 12:08:37 +01:00
Pair < String , int > breakpoint = to_breakpoint ( line ) ;
String source = breakpoint . first ;
int linenr = breakpoint . second ;
2014-02-10 02:10:30 +01:00
2020-12-15 13:04:21 +01:00
if ( source . is_empty ( ) ) {
2018-02-11 12:08:37 +01:00
continue ;
2020-05-14 16:41:43 +02:00
}
2014-02-10 02:10:30 +01:00
2020-02-27 03:30:20 +01:00
script_debugger - > remove_breakpoint ( linenr , source ) ;
2014-02-10 02:10:30 +01:00
2018-02-11 12:08:37 +01:00
print_line ( " Removed breakpoint at " + source + " : " + itos ( linenr ) ) ;
2014-02-10 02:10:30 +01:00
}
} else if ( line = = " h " | | line = = " help " ) {
print_line ( " Built-In Debugger command list: \n " ) ;
2018-02-11 12:08:37 +01:00
print_line ( " \t c,continue \t \t Continue execution. " ) ;
print_line ( " \t bt,backtrace \t \t Show stack trace (frames). " ) ;
2014-02-10 02:10:30 +01:00
print_line ( " \t fr,frame <frame>: \t Change current frame. " ) ;
2018-02-11 12:08:37 +01:00
print_line ( " \t lv,locals \t \t Show local variables for current frame. " ) ;
print_line ( " \t mv,members \t \t Show member variables for \" this \" in frame. " ) ;
print_line ( " \t gv,globals \t \t Show global variables. " ) ;
print_line ( " \t p,print <expr> \t \t Execute and print variable in expression. " ) ;
print_line ( " \t s,step \t \t \t Step to next line. " ) ;
print_line ( " \t n,next \t \t \t Next line. " ) ;
print_line ( " \t fin,finish \t \t Step out of current frame. " ) ;
print_line ( " \t br,break [source:line] \t List all breakpoints or place a breakpoint. " ) ;
print_line ( " \t delete [source:line]: \t Delete one/all breakpoints. " ) ;
print_line ( " \t set [key=value]: \t List all options, or set one. " ) ;
print_line ( " \t q,quit \t \t \t Quit application. " ) ;
2014-02-10 02:10:30 +01:00
} else {
print_line ( " Error: Invalid command, enter \" help \" for assistance. " ) ;
}
}
}
2020-02-27 03:30:20 +01:00
void LocalDebugger : : print_variables ( const List < String > & names , const List < Variant > & values , const String & variable_prefix ) {
2018-02-11 12:08:37 +01:00
String value ;
Vector < String > value_lines ;
const List < Variant > : : Element * V = values . front ( ) ;
2021-07-16 05:45:57 +02:00
for ( const String & E : names ) {
2018-02-11 12:08:37 +01:00
value = String ( V - > get ( ) ) ;
2020-12-15 13:04:21 +01:00
if ( variable_prefix . is_empty ( ) ) {
2021-07-16 05:45:57 +02:00
print_line ( E + " : " + String ( V - > get ( ) ) ) ;
2018-02-11 12:08:37 +01:00
} else {
2021-07-16 05:45:57 +02:00
print_line ( E + " : " ) ;
2018-02-11 12:08:37 +01:00
value_lines = value . split ( " \n " ) ;
for ( int i = 0 ; i < value_lines . size ( ) ; + + i ) {
print_line ( variable_prefix + value_lines [ i ] ) ;
}
}
V = V - > next ( ) ;
}
}
2020-02-27 03:30:20 +01:00
Pair < String , int > LocalDebugger : : to_breakpoint ( const String & p_line ) {
2018-02-11 12:08:37 +01:00
String breakpoint_part = p_line . get_slicec ( ' ' , 1 ) ;
Pair < String , int > breakpoint ;
int last_colon = breakpoint_part . rfind ( " : " ) ;
if ( last_colon < 0 ) {
print_line ( " Error: Invalid breakpoint format. Expected [source:line] " ) ;
return breakpoint ;
}
2020-02-27 03:30:20 +01:00
breakpoint . first = script_debugger - > breakpoint_find_source ( breakpoint_part . left ( last_colon ) . strip_edges ( ) ) ;
2020-02-13 16:42:49 +01:00
breakpoint . second = breakpoint_part . substr ( last_colon ) . strip_edges ( ) . to_int ( ) ;
2018-02-11 12:08:37 +01:00
return breakpoint ;
}
2020-02-27 03:30:20 +01:00
void LocalDebugger : : send_message ( const String & p_message , const Array & p_args ) {
2018-02-05 11:18:49 +01:00
// This needs to be cleaned up entirely.
// print_line("MESSAGE: '" + p_message + "' - " + String(Variant(p_args)));
2014-02-10 02:10:30 +01:00
}
2021-09-22 17:36:40 +02:00
void LocalDebugger : : send_error ( const String & p_func , const String & p_file , int p_line , const String & p_err , const String & p_descr , bool p_editor_notify , ErrorHandlerType p_type ) {
2020-12-15 13:04:21 +01:00
print_line ( " ERROR: ' " + ( p_descr . is_empty ( ) ? p_err : p_descr ) + " ' " ) ;
2018-01-09 17:19:03 +01:00
}
2020-02-27 03:30:20 +01:00
LocalDebugger : : LocalDebugger ( ) {
2018-02-11 12:08:37 +01:00
options [ " variable_prefix " ] = " " ;
2020-02-27 03:30:20 +01:00
// Bind scripts profiler.
scripts_profiler = memnew ( ScriptsProfiler ) ;
Profiler scr_prof (
scripts_profiler ,
[ ] ( void * p_user , bool p_enable , const Array & p_opts ) {
2022-04-05 12:40:26 +02:00
static_cast < ScriptsProfiler * > ( p_user ) - > toggle ( p_enable , p_opts ) ;
2020-02-27 03:30:20 +01:00
} ,
2020-04-02 01:20:12 +02:00
nullptr ,
2022-05-05 20:23:47 +02:00
[ ] ( void * p_user , double p_frame_time , double p_process_time , double p_physics_time , double p_physics_frame_time ) {
static_cast < ScriptsProfiler * > ( p_user ) - > tick ( p_frame_time , p_process_time , p_physics_time , p_physics_frame_time ) ;
2020-02-27 03:30:20 +01:00
} ) ;
register_profiler ( " scripts " , scr_prof ) ;
}
LocalDebugger : : ~ LocalDebugger ( ) {
unregister_profiler ( " scripts " ) ;
2020-05-14 16:41:43 +02:00
if ( scripts_profiler ) {
2020-02-27 03:30:20 +01:00
memdelete ( scripts_profiler ) ;
2020-05-14 16:41:43 +02:00
}
2014-02-10 02:10:30 +01:00
}