flask入門的教程-調試、測試以用優化Debugging, Testing and Profiling

文章轉自 :https://github.com/WapeYang/The-Flask-Mega-Tutorial/blob/master/debugging.rst

感謝原作者的付出

轉載時間爲:2014-05-06


調試,測試以及優化

我們小型的 microblog 應用程序已經足夠的完善了,因此是時候準備儘可能地清理不用的東西。近來,一個讀者反映了一個奇怪的數據庫問題,我們今天將會調試它。這也提醒我們不論我們是多小心以及測試我們應用程序多仔細,我們還是會遺漏一些 bug。用戶是很擅長髮現它們的!

不是僅僅修復此錯誤,然後忘記它,直到我們遇到另一個。我們會採取一些積極的措施,以更好地準備下一個。

在本章的第一部分,我們將會涉及到 調試,我將會展示一些我平時調試複雜問題的技巧。

接着我們想要看看如何衡量我們的測試策略的效果。我們想要清楚地知道我們的測試能夠覆蓋到應用程序的多少,有時候稱之爲測試覆蓋率

最後,我們將會研究下許多應用程序遭受過的一類問題,性能糟糕。我們將會看到 調優 技術,並且發現我們應用程序中哪些部分最慢。

聽起來不錯吧?讓我們開始吧!

Bug

這個問題是由本系列的一個讀者發現,他實現了一個允許用戶刪除自己的 blog 的新函數後遇到這個問題。我們正式的 microblog 版本是沒有這個功能的,因此我們快速的加上它,以便我們可以調試這個問題。

我們使用刪除 blog 的視圖函數如下(文件 app/views.py):

@app.route('/delete/<int:id>')
@login_required
def delete(id):
    post = Post.query.get(id)
    if post == None:
        flash('Post not found.')
        return redirect(url_for('index'))
    if post.author.id != g.user.id:
        flash('You cannot delete this post.')
        return redirect(url_for('index'))
    db.session.delete(post)
    db.session.commit()
    flash('Your post has been deleted.')
    return redirect(url_for('index'))

爲了調用這個函數,我們將會在模板中添加這個刪除鏈接(文件 app/templates/post.html):

{% if post.author.id == g.user.id %}
<div><a href="{{ url_for('delete', id = post.id) }}">{{ _('Delete') }}</a></div>
{% endif %}

現在我們接着繼續,在生產模式下運行我們的應用程序。Linux 和 Mac 用戶可以這麼做:

$ ./runp.py

Windows 用戶這麼做:

flask/Scripts/python runp.py

現在作爲一個用戶,撰寫一篇 blog,接着改變主意刪除它。當你點擊刪除鏈接的時候,錯誤出現了!

我們得到了一個簡短的消息說,應用程序已經遇到了一個錯誤,並已通知管理員。其實這個消息就是我們的 500.html 模版。在生產模式下,當在處理請求的時候出現一個異常錯誤,Flask 會返回一個 500 錯誤模版給客戶端。因爲我們是處於生產模式下,我們看不到錯誤信息或者堆棧軌跡。

現場調試問題

回想 :ref:`testing` 這一章,我們開啓了一些調試服務在應用程序中的生產模式版本。當時我們創造了一個寫入到日誌文件的日誌記錄器,以便應用程序可以在運行時寫入調試或診斷信息。Flask 自身會在結束一個錯誤 500 代碼請求之前寫入不能處理的任何異常的堆棧軌跡。作爲一個額外的選項,我們也開啓了基於日誌的郵件通知服務,它將會向管理員列表發送日誌信息郵件當一個錯誤寫入日誌信息的時候。

因此像上面的 bug,我們能夠從兩個地方,日誌文件和發送的郵件,獲得捕獲的調試信息。

一個堆棧軌跡並不足夠,但是總比沒有好吧。假設我們對問題一點都不知道,我們需要單從堆棧軌跡中之處發生些什麼。這是這個特別的堆棧軌跡的副本:

127.0.0.1 - - [03/Mar/2013 23:57:39] "GET /delete/12 HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1701, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1689, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1687, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1360, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1358, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/microblog/flask/lib/python2.7/site-packages/flask/app.py", line 1344, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/microblog/flask/lib/python2.7/site-packages/flask_login.py", line 496, in decorated_view
    return fn(*args, **kwargs)
  File "/home/microblog/app/views.py", line 195, in delete
    db.session.delete(post)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 114, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1400, in delete
    self._attach(state)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1656, in _attach
    state.session_id, self.hash_key))
InvalidRequestError: Object '<Post at 0xff35e7ac>' is already attached to session '1' (this is '3')

如果你習慣讀別的語言的堆棧軌跡,請注意 Python 的堆棧軌跡是反序的,最下面的是引起異常的發生點。

從上面的堆棧軌跡中我們可以看到異常是被 SQLAlchemy 會話處理代碼觸發的。在堆棧軌跡中,對找出我們代碼最後的執行語句也是十分有幫助的。我們會最後定位到我們的 app/views.py 文件中的 delete() 函數中的 db.session.delete(post) 語句。

我們從上面的情況中可以知道 SQLAlchemy 是不能在數據庫會話中註冊刪除操作。但是我們不知道爲什麼。

如果你仔細看看堆棧軌跡的最下面,你會發現問題好像在 Post 對象最開始綁定在會話 '1',現在我們試着綁定同一對象到會話 '3'中。

如果你搜索這個問題的話,你會發現很多用戶都會遇到這個問題,尤其是使用一個多線程網頁服務器,它們得到兩個請求嘗試同時把同一對象添加到不同的會話。但是我們使用的是 Python 開發網頁服務器,它是一個單線程服務器,因此這不是我們問題的原因。這可能的原因是不知道什麼操作導致兩個會話在同一時間活躍。

如果我們想要了解更多的問題,我們應該嘗試以一種更可控的環境下重現錯誤。幸運的是,在開發模式下的應用程序試圖重現這個問題,它確實重現了。在開發模式中,當發生異常時,我們得到是 Flask web 的堆棧軌跡,而不是 500.html 模板。

web 的堆棧軌跡是十分好的,因爲它允許你檢查代碼並且從服務器上計算表達式。沒有真正理解在這段代碼中到底爲什麼會發生這種事情,我們只是知道某些原因導致一個請求在結束的時候沒有正常刪除會話。因此更好的方案就是找出到底是誰創建這個會話。

使用 Python 調試器

最容易找出誰創建一個對象的方式就是在對象構建中設置斷點。斷點是一個當滿足一定條件的時候中斷程序的命令。此時此刻,這是能夠檢查程序,比如獲取中斷時的堆棧軌跡,檢查或者甚至改變變量值,等等。斷點是 調試器 的特色之一。這次我們將會使用 Python 內置模塊,叫做 pdb

但是我們該檢查哪個類?讓我們回到基於 Web 的堆棧軌跡,再仔細找找。在最底層的堆棧幀中,我們能使用代碼瀏覽器和 Python 控制檯來找出使用會話的類。在代碼中,我們看到我們是在 Session 類中。這像是 SQLAlchemy 中的數據庫會話的基礎類。因爲現在在最底層的堆棧幀正是在會話對象裏,我們能夠在控制檯中得到會話實際的類,通過運行:

>>> print self
<flask_sqlalchemy._SignallingSession object at 0xff34914c>

現在我們知道使用中的會話是通過 Flask-SQLAlchemy 定義的,因此這個擴展可能定義自己的會話類,作爲 SQLAlchemy 會話的一個子類。

現在我們可以到 Flask-SQLAlchemy 擴展的 flask/lib/python2.7/site-packages/flask_sqlalchemy.py 中檢查源代碼並且定位到類_SignallingSession 和它的 __init__() 構造函數,現在我們準備用調試器工作。

有很多方式在 Python 應用程序中設置斷點。最簡單的一種就是在我們想要中斷的程序中寫入如下代碼:

import pdb; pdb.set_trace()

因此我們繼續向前並且暫時在 _SignallingSession 類的構造函數插入斷點(文件 flask/lib/python2.7/site-packages/flask_sqlalchemy.py):

class _SignallingSession(Session):

    def __init__(self, db, autocommit=False, autoflush=False, **options):
        import pdb; pdb.set_trace() # <-- this is temporary!
        self.app = db.get_app()
        self._model_changes = {}
        Session.__init__(self, autocommit=autocommit, autoflush=autoflush,
                         extension=db.session_extensions,
                         bind=db.engine,
                         binds=db.get_binds(self.app), **options)

    # ...

讓我們繼續運行看看會發生什麼:

$ ./run.py
> /home/microblog/flask/lib/python2.7/site-packages/flask_sqlalchemy.py(198)__init__()
-> self.app = db.get_app()
(Pdb)

因爲沒有打印出 “Running on ...” 的信息我們知道服務器實際上並沒有完成啓動過程。中斷可能已經發生了在內部某些神祕的代碼裏面。

最重要的問題是我們需要回答應用程序現在處於哪裏,因爲這將會告訴我們誰在請求創建會話 '1'。我們將會使用 bt 來獲取堆棧軌跡:

(Pdb) bt
  /home/microblog/run.py(2)<module>()
-> from app import app
  /home/microblog/app/__init__.py(44)<module>()
-> from app import views, models
  /home/microblog/app/views.py(6)<module>()
-> from forms import LoginForm, EditForm, PostForm, SearchForm
  /home/microblog/app/forms.py(4)<module>()
-> from app.models import User
  /home/microblog/app/models.py(92)<module>()
-> whooshalchemy.whoosh_index(app, Post)
  /home/microblog/flask/lib/python2.6/site-packages/flask_whooshalchemy.py(168)whoosh_index()
-> _create_index(app, model))
  /home/microblog/flask/lib/python2.6/site-packages/flask_whooshalchemy.py(199)_create_index()
-> model.query = _QueryProxy(model.query, primary_key,
  /home/microblog/flask/lib/python2.6/site-packages/flask_sqlalchemy.py(397)__get__()
-> return type.query_class(mapper, session=self.sa.session())
  /home/microblog/flask/lib/python2.6/site-packages/sqlalchemy/orm/scoping.py(54)__call__()
-> return self.registry()
  /home/microblog/flask/lib/python2.6/site-packages/sqlalchemy/util/_collections.py(852)__call__()
-> return self.registry.setdefault(key, self.createfunc())
> /home/microblog/flask/lib/python2.6/site-packages/flask_sqlalchemy.py(198)__init__()
-> self.app = db.get_app()
(Pdb)

像之前做的,我們會發現在 models.py 的 92 行中存在問題,那裏是我們全文搜索引擎初始化的地方:

whooshalchemy.whoosh_index(app, Post)

奇怪,在這個階段我們並沒有做觸發數據庫會話創建的事情,這看起來好像是初始化 Flask-WhooshAlchemy 的行爲,它創建了一個數據庫會話。

這感覺就像這畢竟不是我們的錯誤,也許某種形式的交互在兩個 Flask 擴展 SQLAlchemy 和 Whoosh 之間。我們能停留在這裏並且尋求兩個擴展的開發者的幫助。或者是我們繼續調試看能不能找出問題的真正所在。我將會繼續,如果大家不感興趣的話,可以跳過下面的內容。

讓我們多看這個堆棧軌跡一眼。我們調用了 whoosh_index(),它反過來調用了 _create_index()。在 _create_index() 中的一行代碼是這樣的:

model.query = _QueryProxy(model.query, primary_key,
            searcher, model)

在這裏的 model 的變量被設置成我們的 Post 類,我們在調用 whoosh_index() 的時候傳入的 Post 類。考慮到這一點,這看起來像是 Flask-WhooshAlchemy 創建了一個 Post.query 封裝,它把原始的 Post.query 作爲參數,並且附加些其它的 Whoosh 特別的東西。接着是最讓人感興趣的一部分。根據上面的堆棧軌跡,下一個調用的函數是 __get__(),這是一個 Python 的 描述符

__get__() 方法是用於實現描述符,它是一個與它們行爲關聯的屬性而不只是一個值。每次被引用,描述符 __get__() 的函數被調用。函數被支持返回屬性的值。在這行代碼中唯一被提及的的屬性就是 query,所以現在我們知道,這個看似簡單的屬性,我們已經在過去使用的生成我們的數據庫查詢不是一個真正的屬性,而是一個描述符。

讓我們繼續往下看看接下來發生什麼。在 __get__() 中的代碼是這個:

return type.query_class(mapper, session=self.sa.session())

這是一個相當暴露一段代碼。比如,User.query.get(id) 我們間接調用 __get__() 方法來提供查詢對象,這裏我們能夠看到這個查詢對象會暗暗地帶來一個數據庫會話。

當 Flask-WhooshAlchemy 使用 model.query 同樣會觸發一個會話,這個會話被創建和與查詢對象關聯。但是這個查詢對象與運行在我們視圖函數中的查詢對象不一樣,Flask-WhooshAlchemy 請求並不是短暫的。Flask-WhooshAlchemy 把這個查詢對象傳入作爲自己的查詢對象,並且存入到 model.query。由於沒有 __set__() 方法對應,新的對象將被存儲爲一個屬性。對於我們的 Post類,這就意味着在 Flask-WhooshAlchemy 完成初始化,我們將會有名稱相同的描述符和屬性。根據優先級,在這種情況下,屬性勝出。

這一切最重要的方面是,這段代碼設置一個持久的屬性,裏面有我們的會話 '1' 。即使應用程序處理的第一個請求將使用這個會話,然後忘掉它,會話不會消失,因爲它仍然是引用由 Post.query 屬性。這是我們的錯誤!

該錯誤是由於混淆(我認爲)描述的類型而造成的。它們看起來像常規屬性,所以人們往往就這樣使用它們。Flask-WhooshAlchemy 開發者只是想創建一個增強的查詢對象用來爲 Whoosh 查詢存儲一些有用的狀態,但是他們沒有意識到引用一個模型的 query 屬性不像看起來的一樣,背後隱藏與一個啓動數據庫的會話的屬性關聯。

迴歸測試

既然現在已經清楚了發生問題的原因所在,我們是不是可以試着重現下問題,爲修復問題做一些準備。如果不願意這麼做的話,那可能只能等到 Flask-WhooshAlchemy 的開發者們去修復,那如果修復版本要等到一年以後?我們是不是要一直等待着,或者直接取消刪除這個功能。

因此爲了準備修復這個問題,我們可以試着去重現這個問題,我們可以試着去創建針對這個問題的測試。爲了創建這個測試,我們需要模擬兩個請求,第一個請求就是查詢一個 Post 對象,模擬我們請求數據爲了在首先顯示 blog。因爲這是第一個會話,我們準備命名這個會話爲 '1'。接着我們需要忘記這個會話創建一個新的會話,就像 Flask-SQLAlchemy 所做的。試着刪除 Post 對象在第二個會話中,這時候應該會觸發這個 bug:

def test_delete_post(self):
    # create a user and a post
    u = User(nickname = 'john', email = '[email protected]')
    p = Post(body = 'test post', author = u, timestamp = datetime.utcnow())
    db.session.add(u)
    db.session.add(p)
    db.session.commit()
    # query the post and destroy the session
    p = Post.query.get(1)
    db.session.remove()
    # delete the post using a new session
    db.session = db.create_scoped_session()
    db.session.delete(p)
    db.session.commit()

現在當我們運行測試的時候失敗會出現:

$ ./tests.py
.E....
======================================================================
ERROR: test_delete_post (__main__.TestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./tests.py", line 133, in test_delete_post
    db.session.delete(p)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/scoping.py", line 114, in do
    return getattr(self.registry(), name)(*args, **kwargs)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1400, in delete
    self._attach(state)
  File "/home/microblog/flask/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1656, in _attach
    state.session_id, self.hash_key))
InvalidRequestError: Object '<Post at 0xff09b7ac>' is already attached to session '1' (this is '3')

----------------------------------------------------------------------
Ran 6 tests in 3.852s

FAILED (errors=1)

修復

爲了解決這個問題,我們需要找到一種連接 Flask-WhooshAlchemy 查詢對象到模型的替代方式。

Flask-SQLAlchemy 的文檔上提到過有一個 model.query_class 屬性包含了用於查詢的類。這實際上是一個更乾淨的方式使得 Flask-SQLAlchemy 使用自定義的查詢類而不是 Flask-WhooshAlchemy 所做的。如果我們配置 Flask-SQLAlchemy 來創建查詢使用 Whoosh 啓用查詢類(它已經是 Flask-SQLAlchemy BaseQuery 的子類),接着我們應該得到跟以前一樣的結果,但是沒有 bug。

我們在 github 上創建了一個 Flask-WhooshAlchemy 項目的分支,那裏我已經實現上面這些修改。如果你想要看這些改變的話,請訪問 github diff,或者下載 修改的擴展 並且安裝它在原始的 flask_whooshalchemy.py 文件所在地。

測試覆蓋率

雖然我們已經有了測試應用程序的測試代碼,但是我們並不知道我們的應用程序有多少地方被測試到。我們需要一個測試覆蓋率的工具來檢查一個應用程序,在執行這個工具後我們能得到一個我們的代碼現在哪些地方被測試到的報告。

Python 有一個測試覆蓋率的工具,我們稱之爲 coverage。讓我們安裝它:

flask/bin/pip install coverage

這個工具可以作爲一個命令行使用或者可以放在腳本里面。我們現在可以先不用考慮如何啓動它。

這有些改變我們需要加入到測試代碼中爲了生成一個覆蓋率的報告(文件 tests.py):

from coverage import coverage
cov = coverage(branch = True, omit = ['flask/*', 'tests.py'])
cov.start()

# ...

if __name__ == '__main__':
    try:
        unittest.main()
    except:
        pass
    cov.stop()
    cov.save()
    print "\n\nCoverage Report:\n"
    cov.report()
    print "HTML version: " + os.path.join(basedir, "tmp/coverage/index.html")
    cov.html_report(directory = 'tmp/coverage')
    cov.erase()

我們開始在腳本的最開始初始化 coverage 模塊。branch = True 參數要求除了常規的覆蓋率分析還需要做分支分析。omit 參數確保不會去獲得我們安裝在虛擬環境和測試框架自身的覆蓋率報告,我們只做我們的應用程序代碼的覆蓋。

爲了收集覆蓋率統計我們只要調用 cov.start(),接着運行我們的單元測試。我們必須從我們的單元測試框架中捕獲以及通過異常,如果腳本不結束的話是沒有機會生成一個覆蓋率報告。在我們從測試中回來後,我們將會用 cov.stop() 停止覆蓋率統計,並且用cov.save() 生成結果。最後,cov.report() 把結果輸出到控制檯,cov.html_report() 生成一個好看的 HTML 報告,cov.erase() 刪除數據文件。

這是運行後的報告例子:

$ ./tests.py
.....F
    ======================================================================
FAIL: test_translation (__main__.TestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./tests.py", line 143, in test_translation
    assert microsoft_translate(u'English', 'en', 'es') == u'Inglés'
AssertionError

----------------------------------------------------------------------
Ran 6 tests in 3.981s

FAILED (failures=1)

Coverage Report:

Name             Stmts   Miss Branch BrMiss  Cover   Missing
------------------------------------------------------------
app/__init__        39      0      6      3    93%
app/decorators       6      2      0      0    67%   5-6
app/emails          14      6      0      0    57%   9, 12-15, 21
app/forms           30     14      8      8    42%   15-16, 19-30
app/models          63      8     10      1    88%   32, 37, 47, 50, 53, 56, 78, 90
app/momentjs        12      5      0      0    58%   5, 8, 11, 14, 17
app/translate       33     24      4      3    27%   10-36, 39-56
app/views          169    124     46     46    21%   16, 20, 24-30, 34-37, 41, 45-46, 53-67, 75-81, 88-109, 113-114, 120-125, 132-143, 149-164, 169-183, 188-198, 203-205, 210-211, 218
config              22      0      0      0   100%
------------------------------------------------------------
TOTAL              388    183     74     61    47%

HTML version: /home/microblog/tmp/coverage/index.html

從上面的報告上可以看到我們測試 47% 的應用程序。我們也從上面得到沒有被測試執行的函數的列表,因此我們必須重新看看這些行,考慮下我們還能編寫些哪些測試。

我們能看到 app/models.py 覆蓋率是比較高(88%),因爲我們的測試集中在我們的模型。app/views.py 覆蓋率是比較低(21%)因爲我們沒有在測試代碼中執行視圖函數。

我們新增加些測試爲了提高覆蓋率:

def test_user(self):
    # make valid nicknames
    n = User.make_valid_nickname('John_123')
    assert n == 'John_123'
    n = User.make_valid_nickname('John_[123]\n')
    assert n == 'John_123'
    # create a user
    u = User(nickname = 'john', email = '[email protected]')
    db.session.add(u)
    db.session.commit()
    assert u.is_authenticated() == True
    assert u.is_active() == True
    assert u.is_anonymous() == False
    assert u.id == int(u.get_id())

def test_make_unique_nickname(self):
    # create a user and write it to the database
    u = User(nickname = 'john', email = '[email protected]')
    db.session.add(u)
    db.session.commit()
    nickname = User.make_unique_nickname('susan')
    assert nickname == 'susan'
    nickname = User.make_unique_nickname('john')
    assert nickname != 'john'
    #...

性能調優

下一個話題就是性能。有什麼比用戶等待很長時間加載頁面更令人沮喪的。我們想要確保我們的應用程序的速度,我們需要一些標準或者尺寸來衡量和分析。

我們使用的技術稱爲 profiling。一個代碼分析器監視正在運行的程序,很像覆蓋工具,而是注意到不是行執行而是多少時間花在每個函數上。在分析階段結束的時候會生成一個報告,裏面列出了所有執行的函數以及每個函數執行了多久。對這個列表從最大到最小的時間排序是一個很好的注意,這樣可以得出我們需要優化的地方。

Python 有一個稱爲 cProfile 的代碼分析器。我們能夠把這個分析器直接嵌入到我們的代碼中,但我們做任何工作之前,搜索是否有人已經完成了集成工作是一個好注意。一個對 “Flask profiler” 的快速搜索得出 Flask 使用的 Werkzeug 模塊有一個分析器的插件,我們將直接使用它。

爲了啓用 Werkzeug 分析器,我們能創建一個像 run.py 的另外一個啓動腳本。讓我們稱它爲 profile.py:

#!flask/bin/python
from werkzeug.contrib.profiler import ProfilerMiddleware
from app import app

app.config['PROFILE'] = True
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions = [30])
app.run(debug = True)

一旦這個腳本運行,每一個請求將會顯示分析器的摘要。這裏就是其中一個例子:

--------------------------------------------------------------------------------
PATH: '/'
         95477 function calls (89364 primitive calls) in 0.202 seconds

   Ordered by: internal time, call count
   List reduced from 1587 to 30 due to restriction <30>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.061    0.061    0.061    0.061 {method 'commit' of 'sqlite3.Connection' objects}
        1    0.013    0.013    0.018    0.018 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py:278(dbapi)
    16807    0.006    0.000    0.006    0.000 {isinstance}
     5053    0.006    0.000    0.012    0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:163(iter_child_nodes)
8746/8733    0.005    0.000    0.005    0.000 {getattr}
      817    0.004    0.000    0.011    0.000 flask/lib/python2.7/site-packages/jinja2/lexer.py:548(tokeniter)
        1    0.004    0.004    0.004    0.004 /usr/lib/python2.7/sqlite3/dbapi2.py:24(<module>)
        4    0.004    0.001    0.015    0.004 {__import__}
        1    0.004    0.004    0.009    0.009 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/__init__.py:7(<module>)
   1808/8    0.003    0.000    0.033    0.004 flask/lib/python2.7/site-packages/jinja2/visitor.py:34(visit)
     9013    0.003    0.000    0.005    0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:147(iter_fields)
     2822    0.003    0.000    0.003    0.000 {method 'match' of '_sre.SRE_Pattern' objects}
      738    0.003    0.000    0.003    0.000 {method 'split' of 'str' objects}
     1808    0.003    0.000    0.006    0.000 flask/lib/python2.7/site-packages/jinja2/visitor.py:26(get_visitor)
     2862    0.003    0.000    0.003    0.000 {method 'append' of 'list' objects}
  110/106    0.002    0.000    0.008    0.000 flask/lib/python2.7/site-packages/jinja2/parser.py:544(parse_primary)
       11    0.002    0.000    0.002    0.000 {posix.stat}
        5    0.002    0.000    0.010    0.002 flask/lib/python2.7/site-packages/sqlalchemy/engine/base.py:1549(_execute_clauseelement)
        1    0.002    0.002    0.004    0.004 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/base.py:124(<module>)
  1229/36    0.002    0.000    0.008    0.000 flask/lib/python2.7/site-packages/jinja2/nodes.py:183(find_all)
    416/4    0.002    0.000    0.006    0.002 flask/lib/python2.7/site-packages/jinja2/visitor.py:58(generic_visit)
   101/10    0.002    0.000    0.003    0.000 flask/lib/python2.7/sre_compile.py:32(_compile)
       15    0.002    0.000    0.003    0.000 flask/lib/python2.7/site-packages/sqlalchemy/schema.py:1094(_make_proxy)
        8    0.002    0.000    0.002    0.000 {method 'execute' of 'sqlite3.Cursor' objects}
        1    0.002    0.002    0.002    0.002 flask/lib/python2.7/encodings/base64_codec.py:8(<module>)
        2    0.002    0.001    0.002    0.001 {method 'close' of 'sqlite3.Connection' objects}
        1    0.001    0.001    0.001    0.001 flask/lib/python2.7/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py:215(<module>)
        2    0.001    0.001    0.002    0.001 flask/lib/python2.7/site-packages/wtforms/form.py:162(__call__)
      980    0.001    0.000    0.001    0.000 {id}
  936/127    0.001    0.000    0.008    0.000 flask/lib/python2.7/site-packages/jinja2/visitor.py:41(generic_visit)

--------------------------------------------------------------------------------

127.0.0.1 - - [09/Mar/2013 19:35:49] "GET / HTTP/1.1" 200 -

在這個報告中每一行的含義如下:

  • ncalls : 這個函數被調用的次數。
  • tottime : 在這個函數中所花費所有時間。
  • percall : 是 tottime 除以 ncalls 的結果。
  • cumtime : 花費在這個函數以及任何它調用的函數的時間。
  • percall : cumtime 除以 ncalls
  • filename:lineno(function) : 函數名以及位置。

有趣的是我們的模板也是作爲函數出現的。這是因爲 Jinja2 的模板是被編譯成 Python 代碼。現在看來暫時我們的應用程序還不存在性能的瓶頸。

數據庫性能

爲了結束這篇,我們最後看看數據庫性能。從上一部分內容中數據庫的處理是在性能分析的報告中,因此我們只需要在數據庫變得越來越慢的時候能夠獲得提醒。

Flask-SQLAlchemy 文檔提到了 get_debug_queries 函數,它返回在請求執行期間所有的查詢的列表。

這是一個很有用的信息。我們可以充分利用這個信息來得到提醒。爲了充分利用這個功能,我們在配置文件中需要啓動它(文件config.py):

SQLALCHEMY_RECORD_QUERIES = True

我們需要設置一個閥值,超過這個值我們認爲是一個慢的查詢(文件 config.py):

# slow database query threshold (in seconds)
DATABASE_QUERY_TIMEOUT = 0.5

爲了檢查是否需要發送警告,我們需要在每一個請求結束的時候進行處理。在 Flask 中,我們只需要設置一個 after_request 函數(文件 app/views.py):

from flask.ext.sqlalchemy import get_debug_queries
from config import DATABASE_QUERY_TIMEOUT

@app.after_request
def after_request(response):
    for query in get_debug_queries():
        if query.duration >= DATABASE_QUERY_TIMEOUT:
            app.logger.warning("SLOW QUERY: %s\nParameters: %s\nDuration: %fs\nContext: %s\n" % (query.statement, query.parameters, query.duration, query.context))
    return response

結束語

本章到這裏就算結束了,本來打算爲這個系列再寫關於部署的內容,但是由於官方的教程已經很詳細了,這裏不再囉嗦了。有需要請訪問 部署選擇

如果你想要節省時間的話,你可以下載 microblog-0.16.zip

本系列準備到這裏就結束了,希望大家喜歡!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章