DBMS_PROFILER

advertisement
DBMS_PROFILER
DBMS_PROFILER
The profiler is a long awaited (by me anyway) feature. It provides us with a source code profiler for our
PL/SQL applications. In the past, you would tune your PL/SQL applications using SQL_TRACE and
TKPROF. This would help you identify and tune your long running SQL, but trying to discover where
the bottlenecks in 5,000 lines of PL/SQL code are (which you might not have even written yourself) was
pretty near impossible. You typically ended up instrumenting the code with lots of calls to
DBMS_UTILITY.GET_TIME to measure elapsed time, in an attempt to find out what was slow.
Well, you no longer have to do this – we have the DBMS_PROFILER package. I am going to demonstrate
how I use it. I myself use little of its total functionality – I just use it to find the really bad areas, and go
straight there. I use it in a very simplistic fashion. It is set up and designed to do much more than
presented here, however.
The statistics gathering takes place in database tables. These tables are set up to hold the statistics for
many different runs of the code. This is OK for some people, but I like to just keep the last run or two,
in there only. Any more than that and it gets too confusing. Sometimes, too much information is just
that – too much information.
Your DBA may have to install the profiler in your database. The procedure for installing this package
is simple:
❑
cd [ORACLE_HOME]/rdbms/admin.
❑
using SVRMGRL you would connect as SYS or INTERNAL.
❑
Run profload.sql.
1161
5254appAK.pdf 1
2/28/2005 6:49:53 PM
Appendix A
In order to actually use the profiler after that, you will need to have the profiling tables installed. You
can install these once per database, but I recommend that developers have their own copy. Fortunately,
the DBMS_PROFILER package is built with invoker rights and unqualified table names, so that we can
install the tables in each schema, and the profiler package will use them correctly. The reason you each
want your own tables, is so that you only see the results of your profiling runs, not those of your coworkers. In order to get a copy of the profiling tables in your schema, you would run
[ORACLE_HOME]\rdbms\admin\proftab.sql in SQL*PLUS. After you run proftab.sql, you'll
need to run profrep.sql as well. This script creates views and packages to operate on the profiler
tables in order to generate reports. This script is found in
[ORACLE_HOME]\plsql\demo\profrep.sql. You should run this in your schema as well, after
creating the tables.
I like to keep a small script around to reset these tables, and clear them out every so often. After I've
done a run or two and have analyzed the results, I run this script. I have the following in a script I call
profreset.sql:
-- uses deletes because of foreign key constraints
delete from plsql_profiler_data;
delete from plsql_profiler_units;
delete from plsql_profiler_runs;
Now we are ready to start profiling. I'm going to demonstrate the use of this package by running two
different implementations of a factorial algorithm. One is recursive, and the other is iterative. We'll use
the profiler to see which one is faster, and what components of the code are 'slow' in each
implementation. The test driver for this is simply:
tkyte@TKYTE816> @profreset
tkyte@TKYTE816> create or replace
2 function fact_recursive( n int ) return number
3 as
4 begin
5
if ( n = 1 )
6
then
7
return 1;
8
else
9
return n * fact_recursive(n-1);
10
end if;
11 end;
12 /
Function created.
tkyte@TKYTE816> create or replace
2 function fact_iterative( n int ) return number
3 as
4
l_result number default 1;
5 begin
6
for i in 2 .. n
7
loop
8
l_result := l_result * i;
9
end loop;
10
return l_result;
11 end;
12 /
1162
5254appAK.pdf 2
2/28/2005 6:49:53 PM
DBMS_PROFILER
Function created.
tkyte@TKYTE816> set serveroutput on
tkyte@TKYTE816> exec dbms_profiler.start_profiler( 'factorial recursive' )
PL/SQL procedure successfully completed.
tkyte@TKYTE816> begin
2
for i in 1 .. 50 loop
3
dbms_output.put_line( fact_recursive(50) );
4
end loop;
5 end;
6 /
30414093201713378043612608166064768844300000000000000000000000000
...
30414093201713378043612608166064768844300000000000000000000000000
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec dbms_profiler.stop_profiler
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec dbms_profiler.start_profiler( 'factorial iterative' )
PL/SQL procedure successfully completed.
tkyte@TKYTE816> begin
2
for i in 1 .. 50 loop
3
dbms_output.put_line( fact_iterative(50) );
4
end loop;
5 end;
6 /
30414093201713378043612608166064768844300000000000000000000000000
...
30414093201713378043612608166064768844300000000000000000000000000
PL/SQL procedure successfully completed.
tkyte@TKYTE816> exec dbms_profiler.stop_profiler
PL/SQL procedure successfully completed.
In order to collect statistics for a profiler run, we must call START_PROFILER. We name each run with
some meaningful name, and then start running the code. I ran the factorial routines 50 times each
before ending the statistics collection for a given run. Now, we are ready to analyze the results.
In [ORACLE_HOME]/plsql/demo there is a script profsum.sql. Don't run it– some of the queries in
that script can take a considerable amount of time to execute (could take hours), and the amount of data
it produces is very large. Below is the modified profsum.sql I use myself. It provides much of the
same information, but the queries execute quickly, and many of the really detailed reports are cut out.
Also, some of the queries would include the timings for the STOP_PROFILER call in it, and others would
not, skewing the observations from query to query. I've adjusted all of the queries to not include the
timings of the profiler package itself.
1163
5254appAK.pdf 3
2/28/2005 6:49:53 PM
Appendix A
My profsum.sql is the following, and this of course is available for download at
http://www.apress.com:
set
set
set
set
set
echo off
linesize 5000
trimspool on
serveroutput on
termout off
column
column
column
column
column
column
column
column
column
column
column
owner format a11
unit_name format a14
text format a21 word_wrapped
runid format 9999
secs format 999.99
hsecs format 999.99
grand_total format 9999.99
run_comment format a11 word_wrapped
line# format 99999
pct format 999.9
unit_owner format a11
spool profsum.out
/* Clean out rollup results, and recreate. */
update plsql_profiler_units set total_time = 0;
execute prof_report_utilities.rollup_all_runs;
prompt
prompt
prompt
prompt
select
from
=
=
====================
Total time
grand_total/1000000000 as grand_total
plsql_profiler_grand_total;
prompt
prompt
prompt
prompt
select
=
=
====================
Total time spent on each run
runid,
substr(run_comment,1, 30) as run_comment,
run_total_time/1000000000 as secs
from (select a.runid, sum(a.total_time) run_total_time, b.run_comment
from plsql_profiler_units a, plsql_profiler_runs b
where a.runid = b.runid group by a.runid, b.run_comment )
where run_total_time > 0
order by runid asc;
prompt
prompt
prompt
prompt
=
=
====================
Percentage of time in each module, for each run separately
1164
5254appAK.pdf 4
2/28/2005 6:49:53 PM
DBMS_PROFILER
select p1.runid,
substr(p2.run_comment, 1, 20) as run_comment,
p1.unit_owner,
decode(p1.unit_name, '', '<anonymous>',
substr(p1.unit_name,1, 20)) as unit_name,
p1.total_time/1000000000 as secs,
TO_CHAR(100*p1.total_time/p2.run_total_time, '999.9') as percentage
from plsql_profiler_units p1,
(select a.runid, sum(a.total_time) run_total_time, b.run_comment
from plsql_profiler_units a, plsql_profiler_runs b
where a.runid = b.runid group by a.runid, b.run_comment ) p2
where p1.runid=p2.runid
and p1.total_time > 0
and p2.run_total_time > 0
and (p1.total_time/p2.run_total_time) >= .01
order by p1.runid asc, p1.total_time desc;
column
prompt
prompt
prompt
prompt
select
secs form 9.99
=
=
====================
Percentage of time in each module, summarized across runs
p1.unit_owner,
decode(p1.unit_name, '', '<anonymous>', substr(p1.unit_name,1, 25)) as
unit_name,
p1.total_time/1000000000 as secs,
TO_CHAR(100*p1.total_time/p2.grand_total, '99999.99') as percentage
from plsql_profiler_units_cross_run p1,
plsql_profiler_grand_total p2
order by p1.total_time DESC;
prompt
prompt
prompt
prompt
select
=
=
====================
Lines taking more than 1% of the total time, each run separate
p1.runid as runid,
p1.total_time/10000000 as Hsecs,
p1.total_time/p4.grand_total*100 as pct,
substr(p2.unit_owner, 1, 20) as owner,
decode(p2.unit_name, '', '<anonymous>', substr(p2.unit_name,1, 20)) as
unit_name,
p1.line#,
( select p3.text
from all_source p3
where p3.owner = p2.unit_owner and
p3.line = p1.line# and
p3.name=p2.unit_name and
p3.type not in ( 'PACKAGE', 'TYPE' )) text
from plsql_profiler_data p1,
plsql_profiler_units p2,
plsql_profiler_grand_total p4
where (p1.total_time >= p4.grand_total/100)
AND p1.runID = p2.runid
and p2.unit_number=p1.unit_number
order by p1.total_time desc;
prompt =
prompt =
prompt ====================
prompt Most popular lines (more than 1%), summarize across all runs
1165
5254appAK.pdf 5
2/28/2005 6:49:53 PM
Appendix A
select p1.total_time/10000000 as hsecs,
p1.total_time/p4.grand_total*100 as pct,
substr(p1.unit_owner, 1, 20) as unit_owner,
decode(p1.unit_name, '', '<anonymous>',
substr(p1.unit_name,1, 20)) as unit_name,
p1.line#,
( select p3.text from all_source p3
where (p3.line = p1.line#) and
(p3.owner = p1.unit_owner) AND
(p3.name = p1.unit_name) and
(p3.type not in ( 'PACKAGE', 'TYPE' ) ) ) text
from plsql_profiler_lines_cross_run p1,
plsql_profiler_grand_total p4
where (p1.total_time >= p4.grand_total/100)
order by p1.total_time desc;
execute prof_report_utilities.rollup_all_runs;
prompt =
prompt =
prompt ====================
prompt Number of lines actually executed in different units (by unit_name)
select p1.unit_owner,
p1.unit_name,
count( decode( p1.total_occur, 0, null, 0)) as lines_executed ,
count(p1.line#) as lines_present,
count( decode( p1.total_occur, 0, null, 0))/count(p1.line#) *100
as pct
from plsql_profiler_lines_cross_run p1
where (p1.unit_type in ( 'PACKAGE BODY', 'TYPE BODY',
'PROCEDURE', 'FUNCTION' ) )
group by p1.unit_owner, p1.unit_name;
prompt
prompt
prompt
prompt
select
from
where
=
=
====================
Number of lines actually executed for all units
count(p1.line#) as lines_executed
plsql_profiler_lines_cross_run p1
(p1.unit_type in ( 'PACKAGE BODY', 'TYPE BODY',
'PROCEDURE', 'FUNCTION' ) )
AND p1.total_occur > 0;
prompt
prompt
prompt
prompt
select
from
where
=
=
====================
Total number of lines in all units
count(p1.line#) as lines_present
plsql_profiler_lines_cross_run p1
(p1.unit_type in ( 'PACKAGE BODY', 'TYPE BODY',
'PROCEDURE', 'FUNCTION' ) );
spool off
set termout on
edit profsum.out
set linesize 131
1166
5254appAK.pdf 6
2/28/2005 6:49:54 PM
DBMS_PROFILER
I have gone out of my way to make that report fit into an 80-column screen, you could be more
generous with some of the column formats if you don't use Telnet frequently.
Now, let's look at the output of our factorial run, the results of running the above profsum.sql script:
Total time
GRAND_TOTAL
----------5.57
This tells us the grand total of our run times across both runs was 5.57 seconds. Next, we'll see a
breakdown by run:
Total time spent on each run
RUNID RUN_COMMENT
SECS
----- ----------- ------17 factorial
3.26
recursive
18 factorial
iterative
2.31
This shows us already that the recursive routine is not nearly as efficient as the iterative version, it took
almost 50 percent longer to execute. Next, we will look at the amount of time spent in each module
(package or procedure) in both runs, and the raw percentage of time by run:
Percentage of time in each module, for each run separately
RUNID RUN_COMMENT UNIT_OWNER UNIT_NAME
SECS PERCEN
----- ----------- ----------- -------------- ------- -----17 factorial
TKYTE
FACT_RECURSIVE
1.87
57.5
recursive
17 factorial
recursive
SYS
DBMS_OUTPUT
1.20
36.9
17 factorial
recursive
<anonymous> <anonymous>
.08
2.5
17 factorial
recursive
<anonymous> <anonymous>
.06
1.9
18 factorial
iterative
SYS
DBMS_OUTPUT
1.24
53.6
18 factorial
iterative
TKYTE
FACT_ITERATIVE
.89
38.5
18 factorial
iterative
<anonymous> <anonymous>
.08
3.4
18 factorial
iterative
<anonymous> <anonymous>
.06
2.7
8 rows selected.
1167
5254appAK.pdf 7
2/28/2005 6:49:54 PM
Appendix A
In this example, we see that in the recursive implementation, 57 percent of our run-time is spent in our
routine, 37 percent in DBMS_OUTPUT, and the rest in miscellaneous routines. In our second run, the
percentages are quite different. Our code is only 38 percent of the total run-time, and that it 38 percent
of a smaller number! This already shows that the second implementation is superior to the first. More
telling is the SECS column. Here, we can see that the recursive routine took 1.87 seconds, whereas the
iterative routine took only .89. If we ignore DBMS_OUTPUT for a moment, we see that the iterative
routine is two times faster than the recursive implementation.
It should be noted that you might not get exactly the same percentages (or even close) on your system.
If you do not have SERVEROUTPUT ON in SQL*PLUS for example, DBMS_OUTPUT might not even show
up on your system. If you run on a slower or faster machine, the numbers will be very different. For
example, when I ran this on my Sparc Solaris machine, the GRAND_TOTAL time was about 1.0 seconds,
and the percentages spent in each section of code were slightly different. Overall, the end result was
much the same, percentage-wise.
Now we can look at the time spent in each module summarized across the runs. This will tell us what
piece of code we spend most of our time in:
Percentage of time in each module, summarized across runs
UNIT_OWNER
----------SYS
TKYTE
TKYTE
<anonymous>
SYS
UNIT_NAME
SECS PERCENTAG
-------------- ----- --------DBMS_OUTPUT
2.44
43.82
FACT_RECURSIVE 1.87
33.61
FACT_ITERATIVE
.89
16.00
<anonymous>
.33
5.88
DBMS_PROFILER
.04
.69
Here, is it obvious we could cut our run-time almost in half by removing the single call to
DBMS_OUTPUT. In fact, if you simply SET SERVEROUTPUT OFF, effectively disabling DBMS_OUTPUT, and
rerun the test, you should find that it drops down to 3 percent or less of the total run-time. Currently
however, it is taking the largest amount of time. What is more interesting than this, is that 33 percent of
the total time is in the recursive routine and only 16 percent in the iterative – the iterative routine is
much faster.
Now, let's look at some more details:
Lines taking more than 1% of the total time, each run separate
RUNID
HSECS
PCT OWNER UNIT_NAME
LINE TEXT
----- ------- ------ ----- -------------- ---- --------------------17 142.47
25.6 TKYTE FACT_RECURSIVE
8 return n*fact_recursive(n-1);
18
68.00
12.2 TKYTE FACT_ITERATIVE
7 l_result := l_result * i;
17
43.29
7.8 TKYTE FACT_RECURSIVE
4 if ( n = 1 )
17
19.58
3.5 SYS
DBMS_OUTPUT
116 a3 a0 51 a5 1c 6e 81 b0
18
19.29
3.5 TKYTE FACT_ITERATIVE
5 for i in 2 .. n
18
17.66
3.2 SYS
DBMS_OUTPUT
116 a3 a0 51 a5 1c 6e 81 b0
17
14.76
2.7 SYS
DBMS_OUTPUT
118 1c 51 81 b0 a3 a0 1c 51
18
14.49
2.6 SYS
DBMS_OUTPUT
118 1c 51 81 b0 a3 a0 1c 51
18
13.41
2.4 SYS
DBMS_OUTPUT
142 :2 a0 a5 b b4 2e d b7 19
17
13.22
2.4 SYS
DBMS_OUTPUT
142 :2 a0 a5 b b4 2e d b7 19
18
10.62
1.9 SYS
DBMS_OUTPUT
166 6e b4 2e d :2 a0 7e 51 b4
1168
5254appAK.pdf 8
2/28/2005 6:49:54 PM
DBMS_PROFILER
17
17
18
18
17
17
18
18
18
17
18
10.46
8.11
8.09
8.02
8.00
7.52
7.22
6.65
6.21
6.13
5.77
1.9
1.5
1.5
1.4
1.4
1.4
1.3
1.2
1.1
1.1
1.0
SYS
SYS
SYS
SYS
SYS
<ano>
<ano>
SYS
<ano>
<ano>
SYS
DBMS_OUTPUT
DBMS_OUTPUT
DBMS_OUTPUT
DBMS_OUTPUT
DBMS_OUTPUT
<anonymous>
<anonymous>
DBMS_OUTPUT
<anonymous>
<anonymous>
DBMS_OUTPUT
166
72
144
72
144
3
3
141
1
1
81
6e b4 2e d :2 a0 7e 51 b4
1TO_CHAR:
8f a0 b0 3d b4 55 6a :3 a0
1TO_CHAR:
8f a0 b0 3d b4 55 6a :3 a0
a0 b0 3d b4 55 6a :3 a0 7e
1ORU-10028:: line length
22 rows selected.
Here, I am printing out the run-time in hundreds of seconds instead of seconds, and showing the
percentages as well. No surprises here – we would expect that line 8 in the recursive routine and line 7
in the iterative routine would be the big ones. Here, we can see that they are. This part of the report
gives you specific lines in the code to zero in on and fix. Notice the strange looking lines of code from
DBMS_OUTPUT. This is what wrapped PL/SQL looks like in the database. It is just a bytecode
representation of the actual source, designed to obscure it from prying eyes like yours and mine.
Now, the next report is similar to the one above, but it aggregates results across runs, whereas the
numbers above show percentages within a run:
Most popular lines (more than 1%), summarize across all runs
HSECS
PCT OWNER UNIT_NAME
LINE TEXT
------- ------ ----- -------------- ---- --------------------142.47
25.6 TKYTE FACT_RECURSIVE
8 return n * fact_recursive(n-1);
68.00
12.2 TKYTE FACT_ITERATIVE
7 l_result := l_result * i;
43.29
7.8 TKYTE FACT_RECURSIVE
4 if ( n = 1 )
37.24
6.7 SYS
DBMS_OUTPUT
116 a3 a0 51 a5 1c 6e 81 b0
29.26
5.3 SYS
DBMS_OUTPUT
118 1c 51 81 b0 a3 a0 1c 51
26.63
4.8 SYS
DBMS_OUTPUT
142 :2 a0 a5 b b4 2e d b7 19
21.08
3.8 SYS
DBMS_OUTPUT
166 6e b4 2e d :2 a0 7e 51 b4
19.29
3.5 TKYTE FACT_ITERATIVE
5 for i in 2 .. n
16.88
3.0 <ano> <anonymous>
1
16.13
2.9 SYS
DBMS_OUTPUT
72 1TO_CHAR:
16.09
2.9 SYS
DBMS_OUTPUT
144 8f a0 b0 3d b4 55 6a :3 a0
14.74
2.6 <ano> <anonymous>
3
11.28
2.0 SYS
DBMS_OUTPUT
81 1ORU-10028:: line length overflow,
10.17
1.8 SYS
DBMS_OUTPUT
147 4f 9a 8f a0 b0 3d b4 55
9.52
1.7 SYS
DBMS_OUTPUT
73 1DATE:
8.54
1.5 SYS
DBMS_OUTPUT
117 a3 a0 1c 51 81 b0 a3 a0
7.36
1.3 SYS
DBMS_OUTPUT
141 a0 b0 3d b4 55 6a :3 a0 7e
6.25
1.1 SYS
DBMS_OUTPUT
96 1WHILE:
6.19
1.1 SYS
DBMS_OUTPUT
65 1499:
5.77
1.0 SYS
DBMS_OUTPUT
145 7e a0 b4 2e d a0 57 b3
20 rows selected.
Lastly, we'll take a look at some of the code coverage statistics. This is useful not only for profiling and
performance tuning, but testing as well. This tells you how many of the statements in the code we have
executed, and shows the percentage of the code that has been 'covered':
1169
5254appAK.pdf 9
2/28/2005 6:49:54 PM
Appendix A
Number of lines actually executed in different units (by unit_name)
UNIT_OWNER
----------SYS
SYS
TKYTE
TKYTE
UNIT_NAME
LINES_EXECUTED LINES_PRESENT
PCT
-------------- -------------- ------------- -----DBMS_OUTPUT
51
88
58.0
DBMS_PROFILER
9
62
14.5
FACT_ITERATIVE
4
4 100.0
FACT_RECURSIVE
3
3 100.0
=
=
====================
Number of lines actually executed for all units
LINES_EXECUTED
-------------67
=
=
====================
Total number of lines in all units
LINES_PRESENT
------------157
This shows that of the 88 statements in the DBMS_OUTPUT package, we executed 51 of them. It is
interesting to note how DBMS_PROFILER counts lines or statements here. It claims that
FACT_ITERATIVE has 4 lines of code, but if we look at the source code:
function fact_iterative( n int ) return number
as
l_result number default 1;
begin
for i in 2 .. n
loop
l_result := l_result * i;
end loop;
return l_result;
end;
I don't see four of anything clearly. DBMS_PROFILER is counting statements, and not really lines of
code. Here, the four statements are:
...
l_result number default 1;
...
for i in 2 .. n
...
l_result := l_result * i;
...
return l_result;
...
1170
5254appAK.pdf 10
2/28/2005 6:49:54 PM
DBMS_PROFILER
Everything else, while necessary to compile and execute the code, was not really executable code, and
therefore, not statements. DBMS_PROFILER can be used to tell us how many executable statements we
have in our code, and how many of them we actually executed.
Caveats
The only caveats I have with regards to DBMS_PROFILER, are the amount of data it generates, and the
amount of your time it can consume.
The small test case we did above generated some 500-plus rows of observations in the
PLSQL_PROFILER_DATA table. This table contains eleven number columns, and so it is not very 'wide',
but it grows rapidly. Every statement executed will cause a row to be added to this table. You will need
to monitor the space you need for this table, and make sure you clear it out every now and again.
Typically, this is not a serious issue, but I have seen this table getting filled with thousands of rows for
extremely complex PL/SQL routines (hundreds of thousands of rows).
The amount of your time it can consume, is a more insidious problem. No matter how much you tune,
there will always be a line of code that consumes the most amount of time. If you remove this line of
code from the top of the list, another one is just waiting to take its place. You will never get a report
from DBMS_PROFILER that says, 'everything ran so fast, I won't even generate a report.' In order to use
this tool effectively, you have to set some goals for yourself – give yourself a finish line. Either set a time
boundary (I will tune this routine to the best of my ability for two hours), or a performance metric
boundary (when the run-time is N units long, I will stop). Otherwise, you will find yourself (as I have
from time to time) spending an inordinate amount of time fine-tuning a routine that just cannot get any
faster.
DBMS_PROFILER is a nice tool with lots of detailed information. It is far too easy to get bogged down in
the details.
Summary
In this section we have covered the uses of the DBMS_PROFILER package. One of its two main uses is
source code profiling to detect where in the code time is being spent, or to compare two different
algorithms. The other major use is as a code coverage tool, to report back the percentage of executable
statements your test routines actually exercised in the application. While 100 percent code coverage
does not assure you of bug free code – it certainly brings you a step closer though.
We also developed a report, based on the example profiler report provided by Oracle. This report
extracts the basic information you need in order to use the DBMS_PROFILER tool successfully. It avoids
the great detail you can go into, providing you with the aggregate view of what happened in your
application, and more details on the most expensive parts. It may be the only report you really need to
use with this tool in order to identify bottlenecks and tune your application.
1171
5254appAK.pdf 11
2/28/2005 6:49:54 PM
Download