]> Sergey Matveev's repositories - public-inbox.git/commitdiff
searchthread: reduce indirection by removing container
authorEric Wong <e@yhbt.net>
Fri, 17 Apr 2020 09:28:49 +0000 (09:28 +0000)
committerEric Wong <e@yhbt.net>
Fri, 17 Apr 2020 09:52:26 +0000 (09:52 +0000)
We can rid ourselves of a layer of indirection by subclassing
PublicInbox::Smsg instead of using a container object to hold
each $smsg.  Furthermore, the `{id}' vs. `{mid}' field name
confusion is eliminated.

This reduces the size of the $rootset passed to walk_thread by
around 15%, that is over 50K memory when rendering a /$INBOX/
landing page.

Documentation/technical/data_structures.txt
lib/PublicInbox/SearchThread.pm
lib/PublicInbox/SearchView.pm
lib/PublicInbox/View.pm
t/thread-cycle.t

index 08dfc7ac0dbfb321eb1d8712b0ca9b792d1e75da..46d5acfff7122a83f4c9ef55be77f7bda5e586c1 100644 (file)
@@ -61,15 +61,13 @@ Per-message classes
   There may be hundreds or thousands of these objects in memory
   at-a-time, so fields are pruned if unneeded.
 
-* PublicInbox::SearchThread::Msg - container for message threading
+* PublicInbox::SearchThread::Msg - subclass of Smsg
   Common abbreviation: $cont or $node
   Used by: PublicInbox::WWW
 
-  The container we use for a non-recursive[1] variant of
+  The structure we use for a non-recursive[1] variant of
   JWZ's algorithm: <https://www.jwz.org/doc/threading.html>.
-  This holds a $smsg and is only used for message threading.
-  This wrapper class may go away in the future and handled
-  directly by PublicInbox::Smsg to save memory.
+  Nowadays, this is a re-blessed $smsg with additional fields.
 
   As with $smsg objects, there may be hundreds or thousands
   of these objects in memory at-a-time.
index 38d1aa6e2c99e961b7caae4562adf840ef5e59b8..60f692b276bb41dd83d292b1b96794e01e31b70a 100644 (file)
@@ -24,7 +24,16 @@ use PublicInbox::MID qw($MID_EXTRACT);
 
 sub thread {
        my ($msgs, $ordersub, $ctx) = @_;
-       my $id_table = {};
+
+       # A. put all current $msgs (non-ghosts) into %id_table
+       my %id_table = map {;
+               # this delete saves around 4K across 1K messages
+               # TODO: move this to a more appropriate place, breaks tests
+               # if we do it during psgi_cull
+               delete $_->{num};
+
+               $_->{mid} => PublicInbox::SearchThread::Msg::cast($_);
+       } @$msgs;
 
        # Sadly, we sort here anyways since the fill-in-the-blanks References:
        # can be shakier if somebody used In-Reply-To with multiple, disparate
@@ -32,36 +41,21 @@ sub thread {
        # always determine ordering when somebody uses multiple In-Reply-To.
        # We'll trust the client Date: header here instead of the Received:
        # time since this is for display (and not retrieval)
-       _add_message($id_table, $_) for sort { $a->{ds} <=> $b->{ds} } @$msgs;
+       _set_parent(\%id_table, $_) for sort { $a->{ds} <=> $b->{ds} } @$msgs;
        my $ibx = $ctx->{-inbox};
        my $rootset = [ grep {
                        !delete($_->{parent}) && $_->visible($ibx)
-               } values %$id_table ];
-       $id_table = undef;
+               } values %id_table ];
        $rootset = $ordersub->($rootset);
        $_->order_children($ordersub, $ctx) for @$rootset;
        $rootset;
 }
 
-sub _get_cont_for_id ($$) {
-       my ($id_table, $mid) = @_;
-       $id_table->{$mid} ||= PublicInbox::SearchThread::Msg->new($mid);
-}
-
-sub _add_message ($$) {
-       my ($id_table, $smsg) = @_;
-
-       # A. if id_table...
-       my $this = _get_cont_for_id($id_table, $smsg->{mid});
-       $this->{smsg} = $smsg;
-
-       # saves around 4K across 1K messages
-       # TODO: move this to a more appropriate place, breaks tests
-       # if we do it during psgi_cull
-       delete $smsg->{num};
+sub _set_parent ($$) {
+       my ($id_table, $this) = @_;
 
        # B. For each element in the message's References field:
-       defined(my $refs = $smsg->{references}) or return;
+       defined(my $refs = $this->{references}) or return;
 
        # This loop exists to help fill in gaps left from missing
        # messages.  It is not needed in a perfect world where
@@ -70,7 +64,8 @@ sub _add_message ($$) {
        my $prev;
        foreach my $ref ($refs =~ m/$MID_EXTRACT/go) {
                # Find a Container object for the given Message-ID
-               my $cont = _get_cont_for_id($id_table, $ref);
+               my $cont = $id_table->{$ref} //=
+                       PublicInbox::SearchThread::Msg::ghost($ref);
 
                # Link the References field's Containers together in
                # the order implied by the References header
@@ -96,22 +91,31 @@ sub _add_message ($$) {
 }
 
 package PublicInbox::SearchThread::Msg;
+use base qw(PublicInbox::Smsg);
 use strict;
 use warnings;
 use Carp qw(croak);
 
-sub new {
+# declare a ghost smsg (determined by absence of {blob})
+sub ghost {
        bless {
-               id => $_[1],
+               mid => $_[0],
                children => {}, # becomes an array when sorted by ->order(...)
-       }, $_[0];
+       }, __PACKAGE__;
+}
+
+# give a existing smsg the methods of this class
+sub cast {
+       my ($smsg) = @_;
+       $smsg->{children} = {};
+       bless $smsg, __PACKAGE__;
 }
 
 sub topmost {
        my ($self) = @_;
        my @q = ($self);
        while (my $cont = shift @q) {
-               return $cont if $cont->{smsg};
+               return $cont if $cont->{blob};
                push @q, values %{$cont->{children}};
        }
        undef;
@@ -122,7 +126,7 @@ sub add_child {
        croak "Cowardly refusing to become my own parent: $self"
          if $self == $child;
 
-       my $cid = $child->{id};
+       my $cid = $child->{mid};
 
        # reparenting:
        if (defined(my $parent = $child->{parent})) {
@@ -148,8 +152,13 @@ sub has_descendent {
 # being folded/mangled by a MUA, and not a missing message.
 sub visible ($$) {
        my ($self, $ibx) = @_;
-       ($self->{smsg} ||= eval { $ibx->smsg_by_mid($self->{id}) }) ||
-        (scalar values %{$self->{children}});
+       return 1 if $self->{blob};
+       if (my $by_mid = $ibx->smsg_by_mid($self->{mid})) {
+               %$self = (%$self, %$by_mid);
+               1;
+       } else {
+               (scalar values %{$self->{children}});
+       }
 }
 
 sub order_children {
index 4fbf59ef2ca080d22db624c393d0db1dcc9084d6..4336e4d9b2d83ed289521450b0e958e0b1f762d0 100644 (file)
@@ -243,8 +243,8 @@ sub search_nav_bot {
 
 sub sort_relevance {
        [ sort {
-               (eval { $b->topmost->{smsg}->{pct} } // 0) <=>
-               (eval { $a->topmost->{smsg}->{pct} } // 0)
+               (eval { $b->topmost->{pct} } // 0) <=>
+               (eval { $a->topmost->{pct} } // 0)
        } @{$_[0]} ]
 }
 
index b6d7acaf64642d31c240febcf431891298e778c3..9b62ed3c3d954d76ab173c4c32bd561b85c62b30 100644 (file)
@@ -297,11 +297,9 @@ sub _th_index_lite {
        my $nr_c = scalar @$children;
        my $nr_s = 0;
        my $siblings;
-       if (my $smsg = $node->{smsg}) {
-               # delete saves about 200KB on a 1K message thread
-               if (my $refs = delete $smsg->{references}) {
-                       ($$irt) = ($refs =~ m/$MID_EXTRACT\z/o);
-               }
+       # delete saves about 200KB on a 1K message thread
+       if (my $refs = delete $node->{references}) {
+               ($$irt) = ($refs =~ m/$MID_EXTRACT\z/o);
        }
        my $irt_map = $mapping->{$$irt} if defined $$irt;
        if (defined $irt_map) {
@@ -310,12 +308,12 @@ sub _th_index_lite {
                $rv .= $pad . $irt_map->[0];
                if ($idx > 0) {
                        my $prev = $siblings->[$idx - 1];
-                       my $pmid = $prev->{id};
+                       my $pmid = $prev->{mid};
                        if ($idx > 2) {
                                my $s = ($idx - 1). ' preceding siblings ...';
                                $rv .= pad_link($pmid, $level, $s);
                        } elsif ($idx == 2) {
-                               my $ppmid = $siblings->[0]->{id};
+                               my $ppmid = $siblings->[0]->{mid};
                                $rv .= $pad . $mapping->{$ppmid}->[0];
                        }
                        $rv .= $pad . $mapping->{$pmid}->[0];
@@ -328,26 +326,26 @@ sub _th_index_lite {
        $attr =~ s!<a\nhref=[^>]+>([^<]+)</a>!$1!s; # no point linking to self
        $rv .= "<b>@ $attr";
        if ($nr_c) {
-               my $cmid = $children->[0]->{id};
+               my $cmid = $children->[0]->{mid};
                $rv .= $pad . $mapping->{$cmid}->[0];
                if ($nr_c > 2) {
                        my $s = ($nr_c - 1). ' more replies';
                        $rv .= pad_link($cmid, $level + 1, $s);
                } elsif (my $cn = $children->[1]) {
-                       $rv .= $pad . $mapping->{$cn->{id}}->[0];
+                       $rv .= $pad . $mapping->{$cn->{mid}}->[0];
                }
        }
 
        my $next = $siblings->[$idx+1] if $siblings && $idx >= 0;
        if ($next) {
-               my $nmid = $next->{id};
+               my $nmid = $next->{mid};
                $rv .= $pad . $mapping->{$nmid}->[0];
                my $nnext = $nr_s - $idx;
                if ($nnext > 2) {
                        my $s = ($nnext - 1).' subsequent siblings';
                        $rv .= pad_link($nmid, $level, $s);
                } elsif (my $nn = $siblings->[$idx + 2]) {
-                       $rv .= $pad . $mapping->{$nn->{id}}->[0];
+                       $rv .= $pad . $mapping->{$nn->{mid}}->[0];
                }
        }
        $rv .= $pad ."<a\nhref=#r$id>$s_s, $s_c; $ctx->{s_nr}</a>\n";
@@ -369,7 +367,7 @@ sub walk_thread ($$$) {
 
 sub pre_thread  { # walk_thread callback
        my ($ctx, $level, $node, $idx) = @_;
-       $ctx->{mapping}->{$node->{id}} = [ '', $node, $idx, $level ];
+       $ctx->{mapping}->{$node->{mid}} = [ '', $node, $idx, $level ];
        skel_dump($ctx, $level, $node);
 }
 
@@ -388,8 +386,8 @@ sub stream_thread_i { # PublicInbox::WwwStream::getline callback
                my $node = shift @$q or next;
                my $cl = $level + 1;
                unshift @$q, map { ($cl, $_) } @{$node->{children}};
-               if (my $smsg = $ctx->{-inbox}->smsg_mime($node->{smsg})) {
-                       return thread_index_entry($ctx, $level, $smsg);
+               if ($ctx->{-inbox}->smsg_mime($node)) {
+                       return thread_index_entry($ctx, $level, $node);
                } else {
                        return ghost_index_entry($ctx, $level, $node);
                }
@@ -407,7 +405,7 @@ sub stream_thread ($$) {
                my $node = shift @q or next;
                my $cl = $level + 1;
                unshift @q, map { ($cl, $_) } @{$node->{children}};
-               $smsg = $ibx->smsg_mime($node->{smsg}) and last;
+               $smsg = $ibx->smsg_mime($node) and last;
        }
        return missing_thread($ctx) unless $smsg;
 
@@ -825,7 +823,7 @@ sub indent_for {
 sub find_mid_root {
        my ($ctx, $level, $node, $idx) = @_;
        ++$ctx->{root_idx} if $level == 0;
-       if ($node->{id} eq $ctx->{mid}) {
+       if ($node->{mid} eq $ctx->{mid}) {
                $ctx->{found_mid_at} = $ctx->{root_idx};
                return 0;
        }
@@ -899,8 +897,8 @@ sub dedupe_subject {
 }
 
 sub skel_dump { # walk_thread callback
-       my ($ctx, $level, $node) = @_;
-       my $smsg = $node->{smsg} or return _skel_ghost($ctx, $level, $node);
+       my ($ctx, $level, $smsg) = @_;
+       $smsg->{blob} or return _skel_ghost($ctx, $level, $smsg);
 
        my $skel = $ctx->{skel};
        my $cur = $ctx->{cur};
@@ -983,7 +981,7 @@ sub skel_dump { # walk_thread callback
 sub _skel_ghost {
        my ($ctx, $level, $node) = @_;
 
-       my $mid = $node->{id};
+       my $mid = $node->{mid};
        my $d = '     [not found] ';
        $d .= '    '  if exists $ctx->{searchview};
        $d .= indent_for($level) . th_pfx($level);
@@ -1006,18 +1004,23 @@ sub _skel_ghost {
 
 sub sort_ds {
        [ sort {
-               (eval { $a->topmost->{smsg}->{ds} } || 0) <=>
-               (eval { $b->topmost->{smsg}->{ds} } || 0)
+               (eval { $a->topmost->{ds} } || 0) <=>
+               (eval { $b->topmost->{ds} } || 0)
        } @{$_[0]} ];
 }
 
 # accumulate recent topics if search is supported
 # returns 200 if done, 404 if not
 sub acc_topic { # walk_thread callback
-       my ($ctx, $level, $node) = @_;
-       my $mid = $node->{id};
-       my $smsg = $node->{smsg} // $ctx->{-inbox}->smsg_by_mid($mid);
-       if ($smsg) {
+       my ($ctx, $level, $smsg) = @_;
+       my $mid = $smsg->{mid};
+       my $has_blob = $smsg->{blob} // do {
+               if (my $by_mid = $ctx->{-inbox}->smsg_by_mid($mid)) {
+                       %$smsg = (%$smsg, %$by_mid);
+                       1;
+               }
+       };
+       if ($has_blob) {
                my $subj = subject_normalized($smsg->{subject});
                $subj = '(no subject)' if $subj eq '';
                my $ds = $smsg->{ds};
@@ -1208,7 +1211,7 @@ sub thread_adj_level {
 sub ghost_index_entry {
        my ($ctx, $level, $node) = @_;
        my ($beg, $end) = thread_adj_level($ctx,  $level);
-       $beg . '<pre>'. ghost_parent($ctx->{-upfx}, $node->{id})
+       $beg . '<pre>'. ghost_parent($ctx->{-upfx}, $node->{mid})
                . '</pre>' . $end;
 }
 
index e9ea0a27223ba5c3c73f481a9bb4e8fcfcfde8c3..d6545c6df9955104ef930cc1f0ea5fd7dc29b79b 100644 (file)
@@ -19,6 +19,7 @@ sub make_objs {
                my $msg = $_;
                $msg->{ds} ||= ++$n;
                $msg->{references} =~ s/\s+/ /sg if $msg->{references};
+               $msg->{blob} = '0'x40; # any dummy value will do, here
                my $simple = Email::Simple->create(header => [
                        'Message-ID' => "<$msg->{mid}>",
                        'References' => $msg->{references},
@@ -100,13 +101,13 @@ done_testing();
 sub thread_to_s {
        my ($msgs) = @_;
        my $rootset = PublicInbox::SearchThread::thread($msgs, sub {
-               [ sort { $a->{id} cmp $b->{id} } @{$_[0]} ] });
+               [ sort { $a->{mid} cmp $b->{mid} } @{$_[0]} ] });
        my $st = '';
        my @q = map { (0, $_) } @$rootset;
        while (@q) {
                my $level = shift @q;
                my $node = shift @q or next;
-               $st .= (" "x$level). "$node->{id}\n";
+               $st .= (" "x$level). "$node->{mid}\n";
                my $cl = $level + 1;
                unshift @q, map { ($cl, $_) } @{$node->{children}};
        }