[BlueLeaf1336]> PROBLEMS> tail>

tail > Try & Error

historyTOP

2005/04/21:作成
2005/04/23:更新
2005/04/24:更新

2005/04/21TOP

だいたいよいのですが、使えば使うほどやっぱり使いにくい場所が目立ってきて邪魔くさいなぁとなってきました。表示に場所を食うとか、元のファイルを別のエディタで開きたいとか、表示しているファイルの入っているフォルダを開きたいとか...

それとは別に、こんなページを見つけました。

Classesを使っていない、ということはつまり、TStringListなんて使っていない。コレはちょっと軽いかどうか試してみる必要があります(威張りすぎ)。

2005/04/23TOP

なんて考えていましたが、そっちの方向ではなくでかいファイルの末尾をもっと早くもっと軽く読むための方法を探すことにしました。

こんなページや、上のページを参考にしたりして。

今、現在は、ファイルのサイズが何MBあろうとも全部をTFileStream経由でTStringListに読み込んでいます。具体的には、監視を開始した時点で全部読み込んで(末尾何行かを表示し)ファイルサイズも記録しておきます。何秒かおき(TTimer使用)に、ファイルサイズを調べて変更があれば再び全部を読み込んで末尾何行かを表示する、という横着処理です。

ところでログファイルを見るという用途に限定するなら、こう考えることもできます。ログファイルとは増える一方であり途中が編集されることは基本的にない、と。ただしファイルサイズが一定以上になればそれまでの内容を捨てるなり別名に変更するなりして新しく作り直すこともある、ということは考える必要がありそうですが。

「現時点のファイルを読み込む際に、直前のファイルサイズまでは読み飛ばせる」んじゃないかと思うわけです。もちろん現時点のファイルサイズが直前のそれよりも大きくなっていれば、という条件付きですが。

必要な部分だけ読み込むTOP

で、実際に読み飛ばせるのかどうか、読み飛ばすと何かよいことがあるかどうか、たとえば速度が向上するとかメモリの使用量が減るなどを試して見ることにします。今のところ、読み飛ばすための方法として2つしか知らないのでそれを試します。その2つとは

です。テスト「50MB程度のファイルを作っておいて後ろから指定したサイズだけを表示」したときの処理にかかった時間とその時に使ったメモリ(こっちは非常に微妙ですが)を調べると方向で考えます。

20050423ForTailWind.zip(166,944bytes)

こんなんになりました。

//-----------------------------------------------------------------------------
//  最後の1MBを読み込む(TFileStream版)
procedure TForm1.Button2Click(Sender: TObject);
var
    FileName: string;
    StringList: TStringList;
    FileStream: TFileStream;
    NewPosition: Int64;
begin
    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');
    //  なければやめる
    if (not FileExists(FileName)) then Exit;
    //  オブジェクト初期化
    StringList := nil;
    FileStream := nil;
    try
        try
            //  読込先
            StringList := TStringList.Create;
            //  ストリームにファイルを割り当てます(他からの全アクセスを許可)
            FileStream := TFileStream.Create(FileName, fmShareDenyNone);
            //  後ろから数えて1MB分の位置まで早送り
            NewPosition := FileStream.Size - 1024 * 1024;
            if (NewPosition < 0) then NewPosition := 0;
            FileStream.Seek(NewPosition, soFromBeginning);
            //  文字列リストに読み込みます(とりあえずコレがゴール)
            StringList.LoadFromStream(FileStream);
        finally
            FileStream.Free;
            StringList.Free;
        end;
    except
        ;
    end;
end;

//-----------------------------------------------------------------------------
//  最後の1MBを読み込む(FileSeek版)
procedure TForm1.Button3Click(Sender: TObject);
var
    FileName: string;
    StringList: TStringList;
    F: File;
    Size, ReadSize: Cardinal;
    SearchRec: TSearchRec;
    Buf: string;
    NewPosition: Int64;
begin
    //  読み込むファイルの名前
    FileName := ChangeFileExt(ParamStr(0), '.txt');
    //  なければやめる
    if (not FileExists(FileName)) then Exit;
    //  オブジェクト初期化
    StringList := nil;
    //  ファイルサイズ取得
    Size := 0;
    if FindFirst(FileName, faAnyFile, SearchRec) = 0 then
    begin
        Size := SearchRec.Size;
        FindClose(SearchRec);
    end;
    //  0以上なら
    if (Size > 0) then
    begin
        try
            //  ファイル割り当て
            AssignFile(F, FileName);
            try
                //  読込先
                StringList := TStringList.Create;
                //  ファイルオープン
                Reset(F, 1);
                //  後ろから数えて1MB分の位置まで早送り
                NewPosition := Size - 1024 * 1024;
                if (NewPosition < 0) then NewPosition := 0;
                //  読み込みバッファのサイズ設定
                SetLength(Buf, Size - NewPosition);
                //  読み込み
                BlockRead(F, PChar(Buf)^, Size - NewPosition, ReadSize);
                StringList.Text := Buf;
            finally
                //  ファイルクローズ
                CloseFile(F);
                StringList.Free;
            end;
        except
            ;
        end;
    end;
end;

FileSeek版がほぼ2倍のメモリ使用量なのは至極当たり前で、原因は、いずれの方法でも最終的にTStringListに読み込むという点にあります。TFileStream版なら直接TStringListに読み込んでいるのに対し、FileSeek版はBlockReadをするために一旦String型のバッファに読み込んでいます。それをコピーする形でTStringListに格納しています。つまり、コピーした瞬間ほぼ1MBのテキストを2つ抱えることになるので当然2倍です。とにかく記述も単純になる「TFileStream.Positionを進めておいて、それ以降をTStringListに読み出す」やり方を採用することにします。

必要な部分だけ読み込む-2TOP

ここまでのテストで「2回目以降の読み込みについては」増分だけを読み込むことで処理時間を短縮できるような印象です。ところで1回目の読み込みについてはどうでしょうか。

今のところ1回目はやっぱり全部読むよなぁな方針ですが、もともとの目的である「ログファイルの末尾の数行を見たい」に立ち戻ると、全部読む必要はないことに気づきます。ただ、条件つきですが。

問題は、ログファイルの1行が何バイトかについて何の制約もないという点です。当たり前です。が、このことがテストでやったような「最後の1MBだけを読む」ような方法で「最後の何行かを読み出す」に置き換えられない原因になります。「読み出したい何行かが何キロバイトか不明」なのです。

ただ普通の(?)ログファイルについて考えてみると、普通メモ帳やテキストエディタで表示します。また1行が何らかの区切りとなっていることが多いようです。当たり前です。改行コードもいれずにずるずるとつなげても何も嬉しいことがありません。メモ帳で表示しても何のことやらわからないだけです。

そう考えると、こんな条件をつけるのがそれほど突拍子もないわけでもなさそうな気がしてきます。「ログファイルの1行は1024バイト以内である」

もちろん「1024」が「2048」でも「512」でもかまわないわけですが、要点としては「1行のサイズがおおよそある一定値以下と考えてもあんまり問題ないよね」ということです。

こう考えることで何が嬉しいかというと、「末尾5行を読み出したい」要求にこたえるために、今までは「全部を読み出して最後の行から5行分取り出していた」のを、「1行は最大で1024バイト(1キロバイト)と仮定して、全部を読まずにファイルの末尾から5行分がほぼ収まっていると考えられる5キロバイト分だけ読み出して、その中から実際の5行分取り出す」ように変更できることになります(文がねじれました)。

多分読み込んでさえしまえば、TStringListで末尾から5行分を取り出す処理にそれほどの速度差はでないと思いますが、サイズがでかくなればなるほどTStringListにテキストを読み込む処理速度の差は大きなものになるはずです。以前のテストで50MBなら読むだけで「1.6秒」かかっていましたが、今回最後の1メガバイトだけを読むようにへんこうすることで「0.03秒」になっています。綺麗に50分の1です。

ログの監視で末尾10行を表示するなら「10キロバイト=0.01メガバイト」だけですむので、もう休みなく読んでも何にも問題がなくなるんじゃないかと錯覚するぐらいのスピードで読み出せそうな気さえしてきます。ヨサゲです。

前置きが長くなりましたが、その方針で次回修正することにします。

2005/04/24TOP

前回との差分だけ読み込んで...なんてややこしいことしなくても単純に必要な分だけ読み込めば充分に早いことがわかったので、そういうことにしました。とりあえず1行は最大でも2KBだとしています。その他の修正としては

実行ファイルとソースコード:20050424tailwind.zip(260,573bytes)

EOFTOP