My current "spare-time" Perl divertisement has been the creation of a puzzle program in Perl/Tk, which mimics a real-life wooden puzzle shown to me by a coworker. Ultimately, the goal is to have the program "solve" the puzzle using recursion, but the first step has been achieved; namely being able to move the various pieces around the board. (If I'm pleased with it, when it's finished I'll post it at Perlmonks).
Recently, while trying to solve the puzzle myself (and failing, thus far), it became clear after a large number of moves that the program had a memory leak somewhere -- it would become less and less responsive, and my initial guess was that one of the Tk Canvas objects was being incorrectly deleted.
That is to say, although the program attempted to $canvas->delete() every object created ($canvas->createLine(), $canvas->createOval(), etc.), there was very likely a location where the delete was not happening, so as to give the sluggish performance one sees when Perl/Tk is overloaded.
To aid me in the search for the offending code, I created the following block:
BEGIN { my $p_id_hash = { }; sub my_create { my ($w, $thing, $pparams) = @_; my $id = $w->$thing(@$pparams); my $pinfo = caller_info(); $p_id_hash->{$id} = [ $thing, @$pinfo ]; return $id; } sub caller_info { my $ln = (caller(1))[2]; my $sub = (caller(2))[3]; return [ $ln, $sub ]; } sub my_delete { my ($w, $id) = @_; my $pinfo = $p_id_hash->{$id} || 0; if (!$pinfo) { error_msg("Attempt to delete ID '$id' twice!"); } else { $w->delete($id); delete $p_id_hash->{$id}; return $id; } } sub show_id_hash { my @keys = sort keys %$p_id_hash; my $total = @keys; print "=" x 79, "\n"; for (my $i = 0; $i < $total; $i++) { my $key = $keys[$i]; my $pval = $p_id_hash->{$key}; my ($thing, $ln, $sub) = @$pval; printf "%3d. %16.16s -- %s[%4d]\n", $i+1, $thing, $sub, $ +ln; } print "Total keys = $total\n"; print "=" x 79, "\n"; } }
The subroutine my_create() gets called in place of normal canvas "create" subroutines, so instead of creating (and later deleting) a rectangle with:
my $id = $canvas->createRectangle(@opts); ... $canvas->delete($id);
the following code does the same thing:
my $id = my_create($canvas, "createRectangle", [ @opts ]); ... my_delete($canvas, $id);
but also keeps track of all objects created in the Canvas. Additionally, an extra button labelled "Show ID Hash" calls show_id_hash() to dump out the table of currently allocated objects, to see whether if it's growing as suspected.
Perl provides the splendid caller functionality so useful for conjuring up stack traces, and, in this case, providing helpful context as to where my_create() and my_delete were called from. Sure enough, each time a move is made, an extra 20 IDs are in the ID hash:
... 32. createText -- main::draw_coordinates[ 936] 33. createText -- main::draw_coordinates[ 936] 34. createText -- main::draw_coordinates[ 936] 35. createText -- main::draw_coordinates[ 936] 36. createText -- main::draw_coordinates[ 936] 37. createText -- main::draw_coordinates[ 936] 38. createText -- main::draw_coordinates[ 936] 39. createText -- main::draw_coordinates[ 936] 40. createText -- main::draw_coordinates[ 936] 41. createText -- main::draw_coordinates[ 936] 42. createText -- main::draw_coordinates[ 936] ...
And this makes sense, because in this particular puzzle, there are 20 squares which, when "coordinate mode" is on, each have a string written to them containing the (x,y) coordinate pair (mostly for debugging). And -- no surprise -- the subroutine which writes this string is called draw_coordinates():
sub draw_coordinates { my ($pboard) = @_; # Delete existing coordinate text my $canvas = $pboard->{'canvas'}; my $ptext = $pboard->{'text'}; map { my_delete($canvas, $_) } @$ptext; $pboard->{'text'} = [ ]; for (my $i = 0; $i < $ncols; $i++) { my $x = $borderx + $i * $cellx + $i * $linex + $linex / 2; for (my $j = 0; $j < $nrows; $j++) { my $y = $bordery + $j * $celly + $j * $liney + $liney / 2; my @opts = ($x, $y, -text => coor_to_square([$i, $j])); push @opts, -anchor => 'nw'; ($coor_font || 0) and push @opts, -font => $coor_font; my $id = my_create($canvas, "createText", [@opts]); push @$ptext, $id; } } }
Now what I was trying to do in the above subroutine was get the hash containing the previously created text IDs:
my $ptext = $pboard->{'text'};
... delete all of them:
map { my_delete($canvas, $_) } @$ptext;
... reassign to an empty anonymous array:
$pboard->{'text'} = [ ];
... and then, in a loop, create the 20 new IDs, and save them for later deletion:
... my $id = my_create($canvas, "createText", [@opts]); push @$ptext, $id;
But there was a sneaky memory leak here! Can you see it?
This debugging exercise reminded me that when writing Perl programs, half the fun is the "sleuthing" one does to find those latent, lurking bugs.
And Perl sure does give you all the tools to make it fun!
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: Adventures in Debugging a Perl/Tk Game
by kyle (Abbot) on Jan 16, 2008 at 04:15 UTC | |
by liverpole (Monsignor) on Jan 16, 2008 at 14:52 UTC | |
by kyle (Abbot) on Jan 16, 2008 at 17:43 UTC | |
Re: Adventures in Debugging a Perl/Tk Game (other leak)
by tye (Sage) on Jan 16, 2008 at 18:31 UTC | |
by liverpole (Monsignor) on Jan 16, 2008 at 20:35 UTC | |
by tye (Sage) on Jan 17, 2008 at 05:04 UTC | |
Re: Adventures in Debugging a Perl/Tk Game
by zentara (Cardinal) on Jan 16, 2008 at 17:56 UTC | |
Re: Adventures in Debugging a Perl/Tk Game
by Limbic~Region (Chancellor) on Jan 18, 2008 at 00:16 UTC | |
by liverpole (Monsignor) on Jan 18, 2008 at 02:53 UTC |