koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

Vimを起動している時間を計測したい

僕は頻繁にVimを終了しているわけではない. ある程度Vimに閉じこもり,必要に応じてシェルに移動したりする. 簡易な処理ならば,vimshell.vimで済ませることも多い. また,Vimを起動したままPCをスリープさせることもよくある. tmuxの1つのペインでVimを立ち上げたまま放置することはしょっちゅうある.

このように,比較的長時間Vimを立ち上げていることが多い. そこで気になるのが,「Vimを起動してからどのくらいの時間が経過したのか」だ. これを確認できれば,作業量を実感でき,「仕事したな~」という満足感を得られるだろう. そこで,Vimを起動している時間を計測するために,以下を.vimrcに記述した.

" Timer {{{
" .vimrcの再読み込みに備えてunlockvarする
" 変数が定義されていなくても,エラーは出ない
unlockvar s:Timer
let s:Timer = {
      \ 'elapsed_time': 0.0,
      \ 'is_stopped': 1
      \}

function! s:Timer.new(name) abort dict
  let timer = copy(self)
  let timer.name = a:name
  call timer.start()
  return timer
endfunction

function! s:Timer.start() abort dict
  if self.is_stopped
    let self.is_stopped = 0
    let self.start_time = reltime()
  endif
endfunction

function! s:Timer.stop() abort dict
  if !self.is_stopped
    let self.elapsed_time += str2float(reltimestr(reltime(self.start_time)))
    let self.is_stopped = 1
  endif
endfunction

function! s:Timer.get_elapsed_time() abort dict
  return (self.is_stopped ? 0.0 : str2float(reltimestr(reltime(self.start_time)))) + self.elapsed_time
endfunction

function! s:Timer.show() abort dict
  let t = s:convert_time(self.get_elapsed_time())
  echomsg printf('%16s: %02d:%02d:%02d.%1d', self.name, t.hour, t.minute, t.second, t.msec)
endfunction

lockvar s:Timer
" }}}

function! s:convert_time(time) abort
  let integer_part = float2nr(a:time)
  let decimal_part = a:time - integer_part
  let t = {
        \ 'hour': integer_part / 3600,
        \ 'msec': float2nr((decimal_part + 0.000001) * 10)
        \}
  let integer_part = integer_part % 3600
  let t.minute = integer_part / 60
  let t.second = integer_part % 60
  return t
endfunction

" .vimrcの再読み込み時にタイマーがリセットされるのを防ぐ
if has('vim_starting')
  let s:startupdate = strftime('%c')
  let s:timer_launched = s:Timer.new('Launched Time')
  let s:timer_active = s:Timer.new('Active Time')
  let s:timer_used = s:Timer.new('Used Time')
endif

augroup Timer
  autocmd!
  autocmd CursorMoved,CursorMovedI * call s:timer_used.start()
  autocmd CursorHold,CursorHoldI,FocusLost * call s:timer_used.stop()
  autocmd FocusGained,WinEnter * call s:timer_active.start()
  autocmd FocusLost * call s:timer_active.stop()
augroup END

function! s:show_time_info() abort
  echo 'Launched at:' s:startupdate
  call s:timer_launched.show()
  call s:timer_active.show()
  call s:timer_used.show()
endfunction
command! -bar ShowTimeInfo  call s:show_time_info()

オブジェクト指向っぽく作ってみた. Vim scriptでもオブジェクト指向はできるのだ.

:ShowTimeInfoとコマンドを実行することで,

  • Vimを起動した日時
  • Vimを起動してからの単純な経過時間
  • Vimがアクティブであった時間
  • Vimを使用した時間

を表示できる.

以下,コードの解説をしよう.

s:Timerはタイマーオブジェクトの雛型である. 必要最低限のタイマーの開始(再開),停止,経過時間の表示のみを実装している. 雛型であるため,一応lockvar s:Timerをして,後から変更が加えられないようにしている.

この雛型はs:Timer.new()というメソッドで自身のクローンを返却する. s:Timer.new()は,要はコンストラクタである. このs:Timer.new()を用いて,

  1. Vimを起動してからの単純な経過時間(PCがスリープの時間を含む)を計測するs:timer_launched
  2. Vimがアクティブであった時間を計測するs:timer_active
  3. Vimを使用した時間を計測するs:timer_used

という3つのタイマーオブジェクトを生成する. また,let s:startupdate = strftime('%c')とすることで,Vimを起動した日時の文字列をs:startupdateに保存しておく.

ただし,strftime('%c')で得られる日付の文字列は,時間のロケールに依存するので,決まったフォーマットで取得したいならば,予めlanguage time Cのようにコマンドを実行して,時間のロケールを変更したり,strftime()に与える文字列を変更すべきである. 例えば,時間のロケールに依存せず,2015-07-23 01:00:00のようなフォーマットで時間を取得したいなら,strftime('%Y-%m-%d %H:%M:%S')と記述するのがよい.

Vimがアクティブである時間は以下のように計測する. Vimがフォーカスを得るとFocusGained,フォーカスが外れるとFocusLostというイベントが発生する. このタイミングでタイマーを開始(再開),停止すればいいだけである.

さて,このFocusGainedFocusLostであるが,GUI版といくつかのコンソール版でしか利用できないらしい. KaoriYa版のCUIVimではFocusGainedFocusLost利用できたが,手元のCygwin版のVimでは利用できなかった. そのような環境では,「Vimを起動してからの単純な経過時間 = Vimがアクティブであった時間」となるだろう.

なお,稀にウィンドウがアクティブになってもFocusGainedを検知できないことがある. 確実にウィンドウがアクティブになったのを検知するために,WinEnterのタイミングでもタイマーを開始(再開)するようにした.

前述の通り,コンソール版のVimでは,FocusGainedFocusLostが利用できないものがある. そこで,Vimがアクティブである時間の代替として,Vimを使用している時間を計測しようと考えた. しかし,これまたいいイベントが無く,CursorHold[I]CursorMoved[I]を利用するしかなかった. CursorHold[I]は,オプション'updatetime'の間,ユーザがキーを押さなかった場合に発生する,つまり,手を止めると発生するイベントである. 反対に,CursorMoved[I]はカーソルが移動した際に発生するイベントである. これらのイベントを利用して,「Vimを使用している時間」とするものを計測するわけだ.

しかし,この「Vimを使用している時間」は,手を動かしてない間は全てVimを使用していないということになってしまう. 実際のコーディングでは,ずっと手を動かしているわけではなく,考え込む時間もある(むしろ,この時間の方が長いはずだ)ため,あまり現実に即した計測方法とはいえないだろう.

実は,定期的にCursorHold[I]を発火させることもでき,「xx秒経過した後,タイマーを停止」ということを実現できるのだが,たかだかVimを使用している時間を計測するためにVimに負荷をかけるのは好きではない. あえて,そうしたいという人がいるならば,上記コード中の

augroup Timer
  autocmd!
  autocmd CursorMoved,CursorMovedI * call s:timer_used.start()
  autocmd CursorHold,CursorHoldI,FocusLost * call s:timer_used.stop()

  " ...

の部分を以下のように変更するとよいだろう.

let s:timer_used.clock = 0
" 30秒以上経過したらタイマーを停止する
let s:timer_used.time_to_stop = 30000
function! s:timer_used.update() abort dict
  if self.clock < self.time_to_stop && !self.is_stopped
    call feedkeys(mode() ==# 'i' ? "\<C-g>\<ESC>" : "g\<ESC>", 'n')
    let self.clock += &updatetime
  else
    call self.stop()
    let self.clock = 0
  endif
endfunction

function! s:timer_used.wakeup() abort dict
  let self.clock = 0
  call self.start()
endfunction

augroup Timer
  autocmd!
  autocmd CursorMoved,CursorMovedI * call s:timer_used.wakeup()
  autocmd CursorHold,CursorHoldI * call s:timer_used.update()
  autocmd FocusLost * call s:timer_used.stop()

  " ...

この例であれば,30秒程度Vimを放置すると,Vimの使用時間の計測を停止するようになる. 30秒「程度」としたのは,updatetimeに設定した値によっては,キッチリ30秒を計測できないからだ. 例えば,updatetimeの値が14000であれば(通常,こんな値を設定することはないと思うが),Vimを放置してから42秒後に使用時間のタイマーが停止する. だからといって,updatetimeの値を10のように小さな値にしてしまうと,10ミリ秒毎にCursorHold[I]が発火するため,PCに非常に負荷がかかる. 上記の設定をするのであれば,Vimを使用している間,updatetime毎に,常にタイマーの処理がされているということを踏まえておこう.

参考