FHEM: Thema Speicherlecks

Begonnen von RichardCZ, 01 April 2020, 11:41:10

Vorheriges Thema - Nächstes Thema

RichardCZ

Da ja neulich angeklungen ist, dass die Grundlagenforschung hier den armen Kindern in Afrika nicht konkret hilft, möchte ich in diesem Thread ein paar Beobachtungen zu dem Thema loswerden, was ja dann vielleicht doch "draussen im Feld" hilft.

Wie schon mal angemerkt, fordert Perl Speicher vom OS nur an und gibt ihn nie wieder frei. Den so angeforderten Speicher verwaltet Perl aber dann mit Freigabe und Wiederverwendung, so dass im Endeffekt ein sauber geschriebenes Perl Programm im Laufe seines Lebens bis zu einem bestimmten Maximalwert der Speichernutzung steigt, aber nicht darüber hinaus. Gibt etliche Perl Apps, die laufen Wochen und Monate und fressen nicht den ganzen Speicher auf.

Wann kommt es zu ungebändigtem Speicherverbrauch?

Der einfachste Fall ist, wenn z.B. ein Array oder noch vielmehr ein Hash irgendwie ins Unendliche wächst. (Hashes brauchen unter Perl relativ viel Speicher, erst recht auf 64bit Systemen). Man schmeisst da u.U. mehr und mehr Keys rein, oder die Values enthalten komplexe Datenstrukturen von denen man vergessen hat, dass sie "noch da sind".

Das können durchaus Datenstrukturen sein, die man mal lokal (my) erzeugt hatte, aber auf die man dann eine Referenz/Pointer gesetzt hatte und nun leben die noch so lange es diese Referenz gibt. Diese Gefahr besteht insbesondere dann, wenn man viele globale Variablen hat, die Referenzen auf solche lokalen Datenstrukturen enthalten können. Check.




Der kompliziertere Fall ist, wenn man es sich vergeigt und zyklische Referenzen im Programm hat. A -> B -> C -> A
Der Perl Garbage Collector ist jetzt schachmatt, weil selbst wenn man B "löscht", sieht er dass da noch eine Referenz von A drauf ist also lässt er B "leben". A kann er auch nicht löschen, weil da noch eine Referenz von C drauf zeigt naja ... man sieht was ich meine.

PBP behandelt auf den Seiten 232 - 234 exakt dieses Thema. Die Ausführungen dort schliessen mit der Empfehlung doch mal weaken aus Scalar::Util zu probieren. Damit wird eine Referenz als "weak" markiert und wenn nun der Perl Garbage Collector so einen Zyklus sieht und eine Referenz darin ist "weak", dann bricht er die dort auf.

$ corelist Scalar::Util
Scalar::Util was first released with perl v5.7.3


Puh - Schwein gehabt!




Und dann noch die Erfahrungswerte von Yours Truly.

Um genau das Problem mit zyklischen Referenzen in den Griff zu bekommen, benutzen etliche Module die mit Perl Datenstrukturen hantieren müssen eigene Strukturen - meist in Form von Hashes - wo sie "Referenzen zählen". Weil dann z.B. ein Data Dumper wissen muss, wo es mal schluss machen sollte:

my $h = {
    a => 1,
    b => 2,
};

$h->{c} = $h;

print Dumper($h);

=>

$ dump.pl
$VAR1 = {
          'a' => 1,
          'c' => $VAR1,
          'b' => 2
        };


Diese Module sind z.B. das Data::Dumper, oder auch Storable. Wenn man Datenstrukturen speichert oder von disk holt (store,retrieve), will man insbesondere bei dem store ja nicht in einen unendlichen rekursiven Abstieg verfallen, nur weil man einer zyklischen Datenstruktur hinterherjagt. Das Modul muss also wissen, welche Referenz "es schon einmal gesehen hat". Das hat zur Folge, dass z.B. ein store $hashref mal eben den doppelten Speicherverbrauch haben kann, den die hinter $hashref liegende Datenstruktur im Speicher einnimmt.

FHEM/98_weekprofile.pm:use Storable qw(dclone);
FHEM/55_DWD_OpenData.pm:use Storable qw(freeze thaw);
FHEM/57_Calendar.pm:use Storable qw(freeze thaw);


Den grep auf "Data::Dumper" mache ich hier jetzt nicht, das wäre länger...




Und wie sieht es konkret in FHEM aus?

Ich fühle mich da noch nicht bewandert genug um definitive Aussagen zu treffen, aber bei meinen Streifzügen durch den Code habe ich schon einige Sonderheiten gesehen. So zum Beispiel (DelayedShutdown):

    my $checkList;
    $checkList = sub {
        return CommandShutdown($cl, $param, undef, 1, $exitValue)
            if (!keys %delayedShutdowns || $waitingFor++ >= $maxShutdownDelay);
        InternalTimer(gettimeofday()+1, $checkList, undef, 0);
    };
    $checkList->();


Auch wenn nun DelayedShutdown nicht unbedingt für ein Speicherloch verantwortlich sein muss, so ist es doch befremdlich, dass die Coderef $checklist, welche ja auf die anonyme Sub zeigt in ebenjener sub verwendet wird. Zyklischer geht es kaum.

Ich fürchte daher fast, dass so etwas in FHEM öfters gang und gäbe ist und möchte mit diesem Text den Sinn/Blick dafür schärfen.

Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

Wzut

Zitat von: RichardCZ am 01 April 2020, 11:41:10
Den grep auf "Data::Dumper" mache ich hier jetzt nicht, das wäre länger...
Übersetze ich jetztmal frei nach : ich darf Data::Dumper für mich verwenden solange ich am Modul schreibe und immer mal wieder Zwischenergbnisse brauche und vor dem einchecken fliegt es wieder raus ?
Wenn ja , dann sollten Module den Dumper nicht dauerhaft aktiv verwenden ? egal ob "nur" zur Anzeige oder zur direkten Speicherung von Werten ? 
Maintainer der Module: MAX, MPD, UbiquitiMP, UbiquitiOut, SIP, BEOK, readingsWatcher

herrmannj

Dieses Thema ist (sehr) praxisrelevant, daher erlaube ich mir Zusätze:

Ich warne vor "weaken" als "Allheilmittel", das sollte nur die letzte Waffe sein wenn man das überhaupt, gar nicht anders in den Griff bekommt. Der refcount von perl ist ja nicht umsonst da. Der Einsatz von "weaken" hat Nebeneffekte und kann (unbedacht eingesetzt) erst die Ursache von Abstürzen sein.

Praktische Hilfe:

Der Autor schreibt sich während des Entwickeln und testen seines so etwas rein:
package memwatch;
use strict;
use warnings;
use utf8;
use Scalar::Util qw( refaddr );

sub new {
  my ($type, $id) = @_;
  my $class = ref $type || $type;
  my $self = {};
  bless $self, $class;
  $self->{id} = $id || 0;
  main::Log3 (undef, 1, sprintf ("memory object %s %s installed", refaddr($self), $self->{id}));
  return $self;
};

sub DESTROY {
  my ($self) = @_;
  main::Log3 (undef, 1, sprintf ("memory object %s %s removed", refaddr($self), $self->{id}));
};


In kritischen Stellen im code des modules macht man dann dies:

my $test = memwatch->new('foo'); # oder bar oder bla ..


Beim testen kann man damit im log sehen das "foo" angelegt wird. Noch viel wichtiger, wenn der Bereich (scope) verlassen wird dann muss eine Meldung erscheinen das der "foo" auch wieder gelöscht wird. Findet das nicht statt dann ist was im Argen und man kann das suchen.

Diese Technik ist (unter anderem) insbesondere bei hash nützlich. Wenn irgendwo ein hash erstellt wird hängt man einfach einen einen key das objekt.
my $hash;
$hash->{'blubber'} =
...
$hash->{'memtest') = memwatch->new('foo');
...

Wenn ich Meldung "..destoyed foo.." nicht im log sehe (wenn ich sie erwarte) darf ich mir sicher sein dass der gesamte hash nicht weggeräumt wird. Damit habe ich ein Leck identifiziert! und ich muss im zweiten Schritt schauen wie ich das weg bekomme. (Hier in der Regel die circular referenz auflösen).

Achtung: auch Objekte (IO::Socket::INET) sind getarnte hashes. Das geht genauso:

$socket->{'mytest'} = memwatch->new('foo');

RichardCZ

Zitat von: herrmannj am 01 April 2020, 12:14:04
Ich warne vor "weaken" als "Allheilmittel", das sollte nur die letzte Waffe sein wenn man das überhaupt, gar nicht anders in den Griff bekommt.

Absolut! Weaken ist für die Fälle wo man weiß, dass man zyklische Referenzen hat, man weiß warum man sie hat bzw. dass man sie braucht und folglich weiß man auch wo man mit weaken ansetzt.

Das Ziel ist es also immer erst sicherzustellen, dass man seine Datenstrukturen kennt und dort nicht (unnötig) Zyklen reinknallt.

Wer weaken ohne weitere Überlegung und ohne genauere analytische Kenntnis seiner Datenstrukturen als magisches Feenpulver benutzt, in der Hoffnung es möge schon irgendwie helfen, wird damit auf die Nase fallen.
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

rudolfkoenig

Weiterhn bitte beim Benutzen von Fremd-Bibliotheken die Doku zu Ende Lesen, z.Bsp. bei XML::DOM ist zum Schluss ein $doc->dispose aufzurufen, genau wegen den zyklischen Referenzen.
Zu $checklist: ich sehe nicht, wieso das zu einem Speicherloch fuehrt. Kannst du mir helfen es zu verstehen?

RichardCZ

Zitat von: rudolfkoenig am 01 April 2020, 13:02:00
Weiterhn bitte beim Benutzen von Fremd-Bibliotheken die Doku zu Ende Lesen, z.Bsp. bei XML::DOM ist zum Schluss ein $doc->dispose aufzurufen, genau wegen den zyklischen Referenzen.
Zu $checklist: ich sehe nicht, wieso das zu einem Speicherloch fuehrt. Kannst du mir helfen es zu verstehen?

Zunächst einmal:

Zitat von: RichardCZ am 01 April 2020, 11:41:10
Auch wenn nun DelayedShutdown nicht unbedingt für ein Speicherloch verantwortlich sein muss, so ist es doch befremdlich, dass die Coderef $checklist, welche ja auf die anonyme Sub zeigt in ebenjener sub verwendet wird. Zyklischer geht es kaum.

Wenn man sich den Code anschaut, dann könnte das $checklist innerhalb der anonymen sub eigentlich erstmal undef sein, weil zum Zeitpunkt des Auftretens war die Sub noch nicht komplett definiert. Dann fragt man sich natürlich, warum es dort steht und nicht gleich ein "undef".

Der Hypothesen sind viele:

1) Vielleicht ist das 2. Argument von InternalTimer irgendein by_reference Gedöns?

my ($tim, $fn, $arg, $waitIfInitNotDone) = @_;

Hmmm... Ne, aber tatsächlich wird da eine Coderef erwartet und die wird auch knallhart ausgeführt if (!$init_done && $waitIfInitNotDone) und nicht etwa if (ref $fn eq 'CODE'), also hmmm. weiß nicht .... Angst.


2) Vielleicht wird $checklist irgendwie für spätere Zeiten gespeichert?

Ah:

my %h = (TRIGGERTIME=>$tim, FN=>$fn, ARG=>$arg, atNr=>++$intAtCnt);
$h{STACKTRACE} = stacktraceAsString(1) if ($addTimerStacktrace);
$intAt{$h{atNr}} = \%h;


$fn ist $checklist. Also ja. In @intAtA wird's auch nochmal gespeichert

push @intAtA, \%h;

und dann verliert sich für mich erstmal die Spur. Wieder Angst.

=>

So lange diese intAt und intAtA Einträge leben, so lange lebt auch die anonyme sub, auch wenn Delayed Shutdown längst verlassen wurde. Das ist ja ok (Closure), aber diese sub hat eben in sich den Pointer auf sich und wie genau stellst Du Dir jetzt vor soll der Perl Garbage Collector die löschen?
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

rudolfkoenig

ZitatSo lange diese intAt und intAtA Einträge leben, so lange lebt auch die anonyme sub, auch wenn Delayed Shutdown längst verlassen wurde.
Das ist genauso, wie gedacht: intAtA speichert die aufzurufenden Funktionen samt Zeit, wann sie aufzurufen sind.
Irgendwennmal ist die gewuenschte Zeit erreicht, Funktion wird aufgerufen, und Eintrag entfernt.
$checklist soll sekuendlich pruefen, ob alle Module bereit sind fuer ein exit.

RichardCZ

Zitat von: rudolfkoenig am 01 April 2020, 13:42:34
Das ist genauso, wie gedacht: intAtA speichert die aufzurufenden Funktionen samt Zeit, wann sie aufzurufen sind.
Irgendwennmal ist die gewuenschte Zeit erreicht, Funktion wird aufgerufen, und Eintrag entfernt.
$checklist soll sekuendlich pruefen, ob alle Module bereit sind fuer ein exit.

Ja - klar. Aber jetzt überleg' Dir, was tatsächlich passiert wenn Du dann sagst "Aus intAtA entfernen".
Dann sieht der garbage Collector eine hashref, wo es eine fn => $fn gibt und jetzt möchte er die $fn entfernen, geht aber nicht, weil irgendwie sagt sein refcount "da ist noch wer"... Naja und dieser "wer" ist in $fn!
Witty House Infrastructure Processor (WHIP) is a modern and
comprehensive full-stack smart home framework for the 21st century.

herrmannj

im delayed shutdown ist das vmtl unkritisch weil.. shutdown.

Bin aber auch ein starker Verfechter da möglichst sauber zu bleiben. Das wär eim übrigen exakt das Einstazszenario für den Testcode oben. Damit kann man das messen. ZDF halt.

Ich verwende den mittlerweile selbst routinemäßig und bin genauso regelmäßg überrascht wo etwas nicht abgeräumt wird obwohl ich mir ganz sicher war dass das ok ist. Danach kommt "Gefahr erkannt, Gefahr gebannt"

rudolfkoenig

ZitatDann sieht der garbage Collector eine hashref, wo es eine fn => $fn gibt und jetzt möchte er die $fn entfernen, geht aber nicht, weil irgendwie sagt sein refcount "da ist noch wer"... Naja und dieser "wer" ist in $fn!

Das leuchtet ein, ich habe die Funktion jetzt als "normale" Funktion ausgelagert.