Programming」カテゴリーアーカイブ

G_PRIORITY_DEFAULT

連休なのに更新ができないままです。
しかたがない、Comipoli Gjs 版で行き詰まっているままですので。
何故こんなに遅いのかまだ解明できていない。

とりあえず他の部分だけでも仕上げておくか。
とやっていたらヒント?かもしれないことを発見。

g_idle_add でバックグラウンド動作ができない場合があった。

その部分だけ抜き出ししてサンプルコード。
PATH 定数は手持ちな巨大サイズの CBZ ファイルに書き換えてね。

const Lang = imports.lang;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio;
const GdkPixbuf = imports.gi.GdkPixbuf;

const PATH = "/home/sasakima-nao/doc/comic/bigfile.cbz";
const SIZE = 120;

const NewArchive = new Lang.Class({
    Name: 'NewArchive',

    _init: function(path) {
        this.namelist = [];
        this.path = path;
        let l = path.split('.');
        let ext = l[l.length-1].toLowerCase();
        //
        if (ext == "cbz") {
            let sp = Gio.Subprocess.new(["unzip", "-Z", path], Gio.SubprocessFlags.STDOUT_PIPE);
            let istream = sp.get_stdout_pipe();
            let dstream = new Gio.DataInputStream({
                base_stream: istream
            });
            for (;;) {
                let [line, len] = dstream.read_line_utf8(null);
                if (line == null) break;
                if (line.startsWith('-')) {
                    let name = line.slice(53);
                    if (GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0)) {
                        this.namelist.push(name);
                    }
                }
            }
            this.namelist.sort();
        }
    },
    get_item: function(num) {
        let sp = Gio.Subprocess.new(["unzip", "-p", this.path, this.namelist[num]], Gio.SubprocessFlags.STDOUT_PIPE);
        let stream = sp.get_stdout_pipe();
        try {
            var pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
        }
        catch(e) {
            print(this.namelist[num]);
            print(e);
        }
        stream.close(null);
        return pixbuf;
    },
    get_length: function() {
        return this.namelist.length;
    }
});

const ThumbnailWindow = new Lang.Class({
    Name: 'ThumbnailDialog',
    Extends: Gtk.Window,

    _init: function() {
        this.parent();
        this.archive = new NewArchive(PATH);
        this.fbox = new Gtk.FlowBox({
            valign: Gtk.Align.START,
            max_children_per_line: 10,
            homogeneous: true,
            selection_mode: Gtk.SelectionMode.BROWSE
        });
        let scroll = new Gtk.ScrolledWindow();
        scroll.add(this.fbox);
        this.add(scroll);
        this.resize(1024, 480);
        this.show_all();
        // idle
        this.gen_func = this.read_zipfile();
        //
        // not G_PRIORITY_DEFAULT or G_PRIORITY_HIGH_IDLE
        //
        //GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
        GLib.idle_add(GLib.PRIORITY_LOW, Lang.bind(this, function() {
            try {
                this.gen_func.next();
                return true;
            } catch(e) {
                return false;
            }
        }));
    },
    read_zipfile: function() {
        let l = this.archive.get_length();
        for (let i=0; i<l; i++) {
            let pixbuf = this.archive.get_item(i);
            this.append_data(pixbuf, i);
            yield 1;
        }
    },
    append_data: function(pixbuf, num) {
        if (pixbuf.get_width() > pixbuf.get_height()) {
            var pix_w = SIZE;
            var pix_h = Math.round(SIZE * pixbuf.get_height() / pixbuf.get_width());
        } else {
            var pix_w = Math.round(SIZE * pixbuf.get_width() / pixbuf.get_height());
            var pix_h = SIZE;
        }
        let minp = pixbuf.scale_simple(pix_w, pix_h, GdkPixbuf.InterpType.TILES);
        let image = new Gtk.Image({
            pixbuf: minp,
            visible: true
        });
        this.fbox.add(image);
    },
    vfunc_hide: function() {
        Gtk.main_quit();
    }
});

Gtk.init(null);
new ThumbnailWindow();
Gtk.main();

抜き出したのに長い…

最初は何も考えずに G_PRIORITY_DEFAULT 指定。
すると for 文で回した時のように全部展開されるまで表示されない。
これじゃ g_idle_add の意味が無い。

バックグラウンド動作させるための関数に何故こんな機能を付けたのだ?
てか、原因がコレだと解るのに一日使ったってばさ。
ということで、皆さん DEFAULT という定数名は疑ったほうがいいかも。

もしかして遅い原因もコレ関係かと探しているけど見当たらない。
this.read_zipfile = function(){}
みたく指定していたのを上記で書き換えたら少し早くなった、気がする…

明日から筆者は仕事だ!
そんなこんなで連休では完成できませんでした。

苗ちゃんフルマカロンまであと一つ、炭酸はギリギリ足りそうだ。
元浜公園に行ったらポケストップが無くなっていてしょんぼり。
いや、息抜きですよ。

Gjs VS PyGObject (Linux time command)

Linuxコマンド集 – 【 time 】 指定したコマンドの実行時間を表示する:ITpro

なんだよ、こんな便利なコマンドがあったのかい!
これで Python と Gjs の速度比較が簡単になる。

function printDateFormat(ms) {
    let date = new Date(ms);
    let s = date.getMinutes() + ":" + date.getSeconds() + ":" + date.getMilliseconds();
    print(s);
}

ってのを Gjs 用に考えたけどいらなかった。

てなわけで、早速 Gio.Subprocess で差が出るか確認してみよう。
前回のコードから不要な部分をバッサリ省いて。

#!/usr/bin/env python3

import gi
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gio, GLib, GdkPixbuf

namelist = []
unzip_list = []

sp = Gio.Subprocess.new(["unzip", "-Z", "test.cbz"], Gio.SubprocessFlags.STDOUT_PIPE);
istream = sp.get_stdout_pipe()
dstream = Gio.DataInputStream(base_stream=istream)
while True:
    line, l = dstream.read_line_utf8()
    if line == None: break
    if line.startswith('-'):
        name = line[53:]
        if GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0):
            namelist.append(name)

for name in namelist:
    sp = Gio.Subprocess.new(["unzip", "-p", "test.cbz", name], Gio.SubprocessFlags.STDOUT_PIPE)
    stream = sp.get_stdout_pipe()
    p = GdkPixbuf.Pixbuf.new_from_stream(stream, None);
    unzip_list.append(p)
    stream.close()

Gjs で書き換えて。

#!/usr/bin/gjs

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const GdkPixbuf = imports.gi.GdkPixbuf;

let namelist = [];
let unzip_list = [];

let sp = Gio.Subprocess.new(["unzip", "-Z", "test.cbz"], Gio.SubprocessFlags.STDOUT_PIPE);
let istream = sp.get_stdout_pipe();
let dstream = new Gio.DataInputStream({base_stream:istream});
for (;;) {
    let [line, l] = dstream.read_line_utf8(null);
    if (line == null) break;
    if (line.startsWith('-')) {
        let name = line.slice(53);
        if (GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0))
            namelist.push(name);
    }
}

for (let i=0; i<namelist.length; i++) {
    let sp = Gio.Subprocess.new(["unzip", "-p", "test.cbz", namelist[i]], Gio.SubprocessFlags.STDOUT_PIPE);
    let stream = sp.get_stdout_pipe();
    let p = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
    unzip_list.push(p);
    stream.close(null);
}

コレを time コマンドで比較。

って一秒すらも差が出ない誤差の範囲ジャン!
Gjs 版 Comipoli が異様に遅い原因は言語のせいでは無いことは判明した。

zipfile module VS unzip command

Comipoli を PyGObject から Gjs に変更すると以前書いた。
実際に作っているんだけど困った、遅すぎる…

もしかして Gjs って遅いの?
いや単に zipfile モジュールが早いだけかも、実験だ。
zipfile を使うから当然 Python で。
blog で Python を書くのは久々のような。

#!/usr/bin/env python3

import time, zipfile, gi
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Gio, GLib, GdkPixbuf

namelist = []
zipfile_list = []
unzip_list = []

sp = Gio.Subprocess.new(["unzip", "-Z", "test.cbz"], Gio.SubprocessFlags.STDOUT_PIPE);
istream = sp.get_stdout_pipe()
dstream = Gio.DataInputStream(base_stream=istream)
while True:
    line, l = dstream.read_line_utf8()
    if line == None: break
    if line.startswith('-'):
        name = line[53:]
        if GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0):
            namelist.append(name)

# zipfile speed
now = time.time()
for name in namelist:
    with zipfile.ZipFile("test.cbz") as o:
        data = o.read(name)
        stream = Gio.MemoryInputStream.new_from_data(data)
        p = GdkPixbuf.Pixbuf.new_from_stream(stream)
        zipfile_list.append(p)
        stream.close()
print(time.time() - now)

# unzip command speed
now2 = time.time()
for name in namelist:
    sp = Gio.Subprocess.new(["unzip", "-p", "test.cbz", name], Gio.SubprocessFlags.STDOUT_PIPE)
    stream = sp.get_stdout_pipe()
    p = GdkPixbuf.Pixbuf.new_from_stream(stream, None);
    unzip_list.append(p)
    stream.close()
print(time.time() - now2)

少し大きめの cbz を用意して。

やはり zipfile モジュールのほうが少し展開が早いんだね。
いや、Gjs で作りかえた Comipoli の遅さはこんなレベルじゃないんだが。
ぶっちゃけ二倍くらい表示に時間が掛かる、とても出せるシロモノではない。

やっぱり Gjs が遅いのかも。
Gjs で同様なサンプルをと思ったけど time.time() の代替は何だ?
g_timer_new とかってバインドされていないのね、GDateTime あたりかな。
それとも他に原因があるかもしれないし、今日はここまで。

ただ Gjs への書き換えをやったおかげで beta12 で多重展開していたのを見つけた。
修正したらスゲェ速くなったので PyGObject のまま beta13 公開。
たまにはこうやって丸ごと書き換えるといいこともあるもんだ。

Gjs unzip

筆者の blog ネタは最近 JavaScript ばかり。
なのに自作アプリの comipoli は Python3 製というのは変だ。

と随分前から思っていたけど、やはり Gjs で作り替えしようと考えた。
PyGObject 製だったのは zipfile モジュールを使っていたからで。

Gjs で作り替えとなるとその代替を考えないと。
cbr で unrar を使っているように unzip を使えばいいだけ、なんだけど。

unzip -Z FILENAME

コマンドでアーカイブの詳細は得られる。
最後の半角空白以降を抜き出せばいいかな。

いやまて、ファイル名に半角空白があった場合はどうなる?
ちゃっと実験用アーカイブを作って実験。

そうなるか、最後の半角空白という手は使えない。
色々試したけど 53 文字以降を取り出せば手持ちファイルは全部イケるようだ。
バージョンによって変わるかもだが、ISF 区切りの 9 番目以降という手もあるし。

ファイルは – で始まっているからコレの見分けは簡単だね。
unzip コマンドの最後に抜き出すファイル名を書けばソレを展開してくれる。
ということでこんなサンプルコードになりました。

#!/usr/bin/gjs

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;

const archive = "test.zip";

let sp = Gio.Subprocess.new(["unzip", "-Z", archive], Gio.SubprocessFlags.STDOUT_PIPE);
let istream = sp.get_stdout_pipe();
let dstream = new Gio.DataInputStream({
    base_stream: istream
});
dstream.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);

function async_callback(source_object, ares) {
    let [line, len] = source_object.read_line_finish_utf8(ares);
    if (line == null) {
        mainloop.quit();
    } else {
        if (line.startsWith('-')) {
            let name = line.slice(53);
            if (GLib.Regex.match_simple("\.js$", name, GLib.RegexCompileFlags.CASELESS, 0)) {
                Gio.Subprocess.new(["unzip", archive, name], Gio.SubprocessFlags.STDOUT_SILENCE);
            }
        }
        source_object.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);
    }
}

let mainloop = new GLib.MainLoop(null, false);
mainloop.run();

read_line_async はメインループが必要だから面倒臭いなぁ。
for で回したほうが簡単だけど非同期にしたかったので。
GMainLoop はプロパティが無いのでこの引数指定にするらしい。

てゆーか、やっと上手くいったぞ。
実は随分前から試していて上手くいかなかっただけなんですけど。

Clutter Constraint

Clutter のドキュメントの中で Constraint がよく解らない。
Google 翻訳によると「制約」、っていったい何の制約なのだろう?
検索検索。

Clutter の Constraint (制約条件) – ふとしの日記

Gjs で動かないって new キーワードの場合引数はプロパティを JSON 指定ですし。
JSON 引数に書き換えたら普通に動いた、多分今は自身で解っていると思うけど。
五年前のこの当事はみんな手探り状態だったししかたがないね。

Clutter.SnapConstraint

えっとつまり。
Constraint ってアクションを他の Actor と連動させるって解釈でいいのかな?
この名前が不適切なのか、実は英語ではコレで合っているということなのかシラネ!

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const ConstraintWin = new Lang.Class({
    Name: 'ConstraintWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            x: 25,
            y: 25,
            width: 50,
            height: 50,
            reactive: true
        });
        this.blue = new Clutter.Actor({
            background_color: Clutter.Color.new(0, 0, 255, 255),
            width: 50,
            height: 50,
        });
        // DnD
        this.red.add_action(new Clutter.DragAction());
        // Constraint
        let bind = new Clutter.BindConstraint({
	        coordinate: Clutter.BindCoordinate.POSITION,
	        offset: 50,
            source: this.red
        });
        this.blue.add_constraint(bind);
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.red);
        stage.add_child(this.blue);
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new ConstraintWin(app);
});
app.run(null);

で赤いブロックをドラッグする。
うん、見事にオフセットされたまま連動してドラッグされるんだね。

ClutterAlignConstraint は Totem のボタン類みたいな用途で使えそう。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const TotemLikeWin = new Lang.Class({
    Name: 'TotemLikeWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        //
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            width: 200,
            height: 50,
            reactive: true
        });
        stage.add_child(this.red);
        // Constraint
        let x = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.X_AXIS,
	        factor: 0.5,
            source: stage
        });
        let y = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.Y_AXIS,
	        factor: 0.8,
            source: stage
        });
        this.red.add_constraint(x);
        this.red.add_constraint(y);
        //
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new TotemLikeWin(app);
});
app.run(null);

いくらリサイズしても常に同じ位置に貼り付ける動作がこんなにアッサリ!
多分こういうのが本来の使い方だと思う。