Fork me on GitHub

分类 工具技巧 下的文章

为什么Visual Studio不能在调试时使用lambda表达式

一、引子

相信很多人都有这样的经历,在Visual Studio的Watch窗口中查看一个List类型的变量,如果这个List中的元素太多,有时想使用LINQ的Where方法筛选一下,类似于下图中的代码:

code

假设我们断在图示断点所在位置,这时我们在Watch窗口中查看personList变量是没问题的,但是如果我只想查看满足Age==20personList,则会看到Visual Studio提示错误:Expression cannot contain lambda expressions

watch

这个问题一直困扰我很久,为什么在Visual Studio中不能在Watch窗口中使用lambda表达式呢?今天忍不住Google了一把,才发现这并不是我一个人的需求,网上有大把大把的人表示不理解为什么Visual Studio不提供这一功能,以至于Visual Studio官方的uservoice上积累了将近一万人的投票,建议Visual Studio引入这个功能。并且好消息是,在Visual Studio最新的2015版本中终于加入了这一特性。

二、原因初探

查看在uservoice下面的回复,我意识到为什么这一特性姗姗来迟,原来是因为这确实是一件很复杂的事。在Stackoverflow上也有很多相关的提问,其中有一篇JaredPar在评论中给出了很精彩的回复。我将他的回复原文摘抄下来放在下面:

No you cannot use lambda expressions in the watch / locals / immediate window. As Marc has pointed out this is incredibly complex. I wanted to dive a bit further into the topic though. What most people don't consider with executing an anonymous function in the debugger is that it does not occur in a vaccuum. The very act of definining and running an anonymous function changes the underlying structure of the code base. Changing the code, in general, and in particular from the immediate window, is a very difficult task. Consider the following code.

void Example() {
  var v1 = 42;
  var v2 = 56; 
  Func<int> func1 = () => v1;
  System.Diagnostics.Debugger.Break();
  var v3 = v1 + v2;
}

This particular code creates a single closure to capture the value v1. Closure capture is required whenever an anonymous function uses a variable declared outside it's scope. For all intents and purposes v1 no longer exists in this function. The last line actually looks more like the following

var v3 = closure1.v1 + v2;

If the function Example is run in the debugger it will stop at the Break line. Now imagine if the user typed the following into the watch window

(Func<int>)(() => v2);

In order to properly execute this the debugger (or more appropriatel the EE) would need to create a closure for variable v2. This is difficult but not impossible to do. What really makes this a tough job for the EE though is that last line. How should that line now be executed? For all intents and purposes the anonymous function deleted the v2 variable and replaced it with closure2.v2. So the last line of code really now needs to read

var v3 = closure1.v1 + closure2.v2;

Yet to actually get this effect in code requires the EE to change the last line of code which is actually an ENC action. While this specific example is possible, a good portion of the scenarios are not. What's even worse is executing that lambda expression shouldn't be creating a new closure. It should actually be appending data to the original closure. At this point you run straight on into the limitations ENC. My small example unfortunately only scratches the surface of the problems we run into. I keep saying I'll write a full blog post on this subject and hopefully I'll have time this weekend.

大意是讲由于lambda表达式涉及到了C#的闭包,而由于C#闭包的特性,导致在调试时如果在Watch窗口中输入lambda表达式将会修改原有代码结构,这并不是不可能,但是确实是一件非常困难且巨大的工程。

三、理解C#的lambda表达式和闭包

好奇心驱使我使用.NET Reflector查看了一下生成的exe文件,看到了下面这样的代码:

[CompilerGenerated]
private static bool <Main>b__0(Person x)
{
    return x.Age < 20;
}

[CompilerGenerated]
private static void <Main>b__1(Person x)
{
    Console.WriteLine(x.Name);
}

private static void Main(string[] args)
{
    personList.Where<Person>(Program.<Main>b__0).ToList<Person>().ForEach(Program.<Main>b__1);
    Console.Read();
}

可以看到lambda表达式被转换成了带有[CompilerGenerated]特性的静态方法,这也就意味着如果我们在调试的时候在Watch中每写一个lambda表达式,Visual Studio都需要动态的创建一个新的静态方法出来然后重新编译。这恐怕是最简单的情形,对调试器而言只是插入一个新的方法然后重新编译而已,Visual Studio已经支持运行时修改代码了,所以这种情形实现起来应该是没问题的。但是复杂就复杂在并不是每个lambda表达式都是这样简单,闭包特性的引入使得lambda表达式中的代码不再只是上下文无关的一个静态方法了,这使得问题变得越来越有意思。我们看下面的示例代码,其中用到了C#的闭包特性:

static void TestOne()
{
    int x = 1, y = 2, z = 3;
    Func<int> f1 = () => x;
    Func<int> f2 = () => y + z;
    Func<int> f3 = () => 3;

    x = f2();
    y = f1();
    z = f1() + f2();

    Console.WriteLine(x + y + z);
}

再看反编译的代码:

[CompilerGenerated]
private sealed class <>c__DisplayClass6
{
    // Fields
    public int x;
    public int y;
    public int z;

    // Methods
    public int <TestOne>b__4()
    {
        return this.x;
    }

    public int <TestOne>b__5()
    {
        return this.y + this.z;
    }
}

[CompilerGenerated]
private static int <TestOne>b__6()
{
    return 3;
}

private static void TestOne()
{
    <>c__DisplayClass6 CS$<>8__locals7 = new <>c__DisplayClass6();
    CS$<>8__locals7.x = 1;
    CS$<>8__locals7.y = 2;
    CS$<>8__locals7.z = 3;
    Func<int> f1 = new Func<int>(CS$<>8__locals7.<TestOne>b__4);
    Func<int> f2 = new Func<int>(CS$<>8__locals7.<TestOne>b__5);
    Func<int> f3 = new Func<int>(Program.<TestOne>b__6);
    CS$<>8__locals7.x = f2();
    CS$<>8__locals7.y = f1();
    CS$<>8__locals7.z = f1() + f2();
    Console.WriteLine((int) (CS$<>8__locals7.x + CS$<>8__locals7.y + CS$<>8__locals7.z));
}

从生成的代码可以看到编译器帮我们做了很多事情,lambda表达式只是语法糖而已。简单的lambda表达式被转换成带有[CompilerGenerated]特性的静态方法,使用闭包特性的lambda表达式被转换成带有[CompilerGenerated]特性的封装(sealed)类,并将所有涉及到的局部变量移到该类中作为该类的public字段,表达式本身移到该类中作为该类的public方法。而且下面所有对闭包涉及到的变量的操作都转换成了对类的字段的操作。 所以回到上文中JaredPar给出的解释,当我们在调试器中输入lambda表达式(Func)(() => v2)时可能存在两种不同的解决方法:

  1. 该方法中已经存在一个闭包,则需要将v2变量提到该闭包类中,并添加一个() => v2方法;
  2. 新建一个闭包类,将v2变量移到该类中,并添加一个() => v2方法;

但是这还远远不够,所有涉及到v2变量的操作,都需要调整为类似于CS$<>8__locals7.v2这样的代码,哦,想想就觉得好麻烦。

四、解决方法

先不谈Visual Studio 2015已经引入这个特性吧,我还没尝试过,也不知道是怎么实现的。暂时我还是在用Visual Studio 2010,所以如果想在老版本中提供这样的功能,就得另谋他策。下面是一些已知的解决方法,我仅是记下来而已,也没尝试过,等试过再写篇新博客记录下吧。

  1. 使用动态LINQ 参考链接中列出了一些动态LINQ的实现,其中用的比较多的应该是Scott提供的LINQ动态查询库,这样在调试时可以使用personList.Where("Age = @0", 20)来替代personList.Where(x => x.Age == 20)
  2. 使用一个VS的开源插件:Extended Immediate Window for Visual Studio
  3. 升级成Visual Studio 2015 ;-)

参考

  1. Allow the evaluation of lambda expressions while debugging
  2. VS debugging “quick watch” tool and lambda expressions
  3. 闭包在.NET下的实现,编译器玩的小把戏
  4. C#与闭包
  5. LINQ to SQL实战 动态LINQ的几种方法
  6. Extended Immediate Window for Visual Studio (use Linq, Lambda Expr in Debugging)
扫描二维码,在手机上阅读!

通过FiddlerScript实现根据条件重发请求

Fiddler是个强大的Web调试工具,具体的功能不在此多述,可以参考后面的链接以及Fiddler官网的手册。本文主要介绍Fiddler的重发请求功能,并通过自定义脚本实现根据条件来重发请求。 在进行Web调试时,经常会遇到浏览器请求正常但是程序请求异常的情况,这时我们常常需要使用Fiddler对比这两个请求的异同,然后将一个请求改变参数或HTTP头进行重发来查看返回结果的差异,这样可以确定哪个参数或哪个HTTP头导致的问题。如下图重发可以有多种不同的选择,常用的有三个:

  1. Reissue Requests: 直接重发选定请求
  2. Reissue and Edit: 重发选定请求,并在请求之前断点,可以对请求进行修改
  3. Reissue from Composer: 将选定请求送到Composer窗口,和将请求拖拽到Composer效果是一样的,在Composer窗口中可以对请求有更精确的控制

replay

只简单的重发指定请求,或在指定请求上进行编辑往往是不够的,在项目中我们偶尔会遇到这样的情形:先发送请求A,然后根据请求A结果中的某个值来发送请求B,譬如有这样的两个接口:get_random_server.php接口通过接收的数据随机返回一个服务器ID,get_data.php接口则根据刚刚的服务器ID来获取数据。下面是一个示例:

  1. localhost/get_random_server.php?data=Hello -> 返回JSON结果:{ success: true, sid: 2 }
  2. localhost/get_data.php?sid=2

这个时候Fiddler的可扩展性就能大显神威了,可以通过两种方式实现Fiddler的扩展:FiddlerScript和插件机制,这里使用FiddlerScript就足够应付了。在Fiddler的菜单项Rules中选择Customize Rules...就可以打开Fiddler的自定义脚本文件CustomRules.js,该脚本一般保存在\Documents\Fiddler2\Scripts目录下。我推荐使用Fidder ScriptEditor进行脚本的编辑,Fidder ScriptEditor具有语法高亮和语法检查的功能,并在右侧面板提供了Fiddler内置的函数列表。 通过展开浏览右侧的函数列表,就基本上可以大概的了解到几个可能会用到的函数了:

  1. FiddlerApplication.oProxy.SendRequest
  2. FiddlerApplication.oProxy.SendRequestAndWait
  3. FiddlerObject.utilIssueRequest

我们先通过下面的代码来练练手,将下面的代码拷贝到CustomRules.js中并保存,Fidder ScriptEditor会自动检查语法错误,并重新加载脚本,无需重启Fiddler脚本即可生效。CustomRules.js使用的是JScript.Net语法,对于Javascipt或.C#程序员应该可以很快上手。这时在Fiddler中随便选择一条请求,点击右键,会发现最上面多了一个选择项Test Send Request,选择该项可以达到和Reissue Requests同样的功能,重发指定请求。

    public static ContextAction("Test Send Request")
    function SendRequest(oSessions: Session[]) {
        
        if (oSessions.Length == 0) return;
        FiddlerApplication.Log.LogString("Sending...");
        
        var selected: Session = oSessions[0];
        
        var oSD = new System.Collections.Specialized.StringDictionary();
        var res = FiddlerApplication.oProxy.SendRequestAndWait(selected.oRequest.headers, selected.RequestBody, oSD, null);
        FiddlerApplication.Log.LogString("Request has been Send.");
        FiddlerApplication.Log.LogString(res.GetResponseBodyAsString());
    }

SendRequest/SendRequestAndWait函数有一个不方便之处,他的两个参数oHeadersarrRequestBodyBytes分别是HTTPRequestHeadersByte[]类型,为了调用这个方法必须将HTTP的header和body转换为这两个类型,不如字符串来的简便。这个时候utilIssueRequest函数正好满足我们的定制需要,可以精确的控制一个请求的细节,类似于Composer中的Raw。下面的代码是一个使用utilIssueRequest函数的实例,具体的HTTP请求以字符串的形式拼接起来。

    public static ContextAction("Probe this!")
    function ProbeSession(oSessions: Session[]) {
        
        if (oSessions.Length == 0) return;
        FiddlerApplication.Log.LogString("Probing...");
        
        var selected: Session = oSessions[0];
        var raw = "";
        
        // methods
        var method:String = selected.RequestMethod;
        var url:String = selected.fullUrl;
        var protocol = "HTTP/1.1";
        FiddlerApplication.Log.LogString(method + " " + url + " " + protocol);
        raw += method + " " + url + " " + protocol + "\r\n";
        
        // headers
        for (var i:int = 0; i < selected.oRequest.headers.Count(); i++) {
            var header = selected.oRequest.headers[i];
            FiddlerApplication.Log.LogString(header);
            raw += header + "\r\n";
        }
        
        // body
        FiddlerApplication.Log.LogString("---");
        var body = selected.GetRequestBodyAsString();
        FiddlerApplication.Log.LogString(body);
        raw += "\r\n" + body;
        
        FiddlerObject.utilIssueRequest(raw);
        FiddlerApplication.Log.LogString("Request has been Send.");
    }

HTTP请求的格式如下:

POST http://localhost/get_random_server.php HTTP/1.1
Host: localhost
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36
Accept-Encoding: gzip,deflate,sdch
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4
Content-Type: application/x-www-form-urlencoded
Content-Length: 23

data=%E4%BD%A0%E5%A5%BD

后面的工作就水到渠成了,通过SendRequestAndWait获取请求A的结果,解析请求A结果获取sid参数,然后拼接HTTP请求调用utilIssueRequest函数,此处从略。

参考

  1. FiddlerScript Editor
  2. Search Sequential Pages for Target String
  3. HTTP协议详解
  4. Fiddler 教程
扫描二维码,在手机上阅读!

WordPress打开速度巨卡的解决方法

自从搭建完了这个WordPress博客站点后就一直觉得首页加载好慢,没怎么仔细深究,毕竟自己一个人写着玩的,大多数都是在后台操作,后台的速度还可以。今天想着把一篇博客分享给同学看看,突然发现页面加载超过了1分钟,实在是让人忍无可忍了。 起先以为是服务器问题,后来在百度上搜索“WordPress打开速度慢”关键词,才发现原来并不是我才遇到这个问题,相当多的WordPress站点都变慢了。而这个问题原因竟然是Google在大天朝被封导致的。由于WordPress使用了Google的公共库和字体库,所有对*.googleapis.com的请求都超时了。 解决方法有两个:

  • 禁用Google字体
  • 使用其他的公共库和字体库

我使用的是360的服务,搜索全站,把googleapis.com全部替换为useso.com,问题解决。

参考

  1. 360网站卫士推出google字体加速方案
  2. wordpress突然变慢,都是googleapi字体惹的祸
  3. 解决 Google 被封导致公共库和字体库无法访问
扫描二维码,在手机上阅读!

使用SVN的8个技巧

1. 使用SVN钩子强制提交注释

一个好的SVN实践是文件提交时要求必须填写注释,并注明相关修改信息,如bug号、任务描述等,内容按照约定编写。这样在后期的代码审核和回溯过程中会非常方便,可以更快的定位到具体代码的修改记录。

所谓SVN钩子就是一些与版本库事件发生时触发的程序,例如新修订版本的创建,或者是未版本化属性的修改。目前subversion提供了如下几种钩子:start-commitpre-commitpost-commitpre-unlockpost-unlockpre-lockpost-lockpre-revprop-changepost-revprop-change,其中我们修改post-commit脚本即可实现强制提交注释的功能。

如下是一个实现强制提交注释的post-commit脚本例子:

REPOS="$1"
TXN="$2"

# Make sure that the log message contains some text.
SVNLOOK=/usr/bin/svnlook
LOGMSG=`$SVNLOOK log -t "$TXN" "$REPOS" | wc -m`

if [ "$LOGMSG" -lt 4 ]
then
    echo "拜托,写点注释吧!" 1>&2
    exit 1
fi

exit 0

2. 创建本地仓库

有时候我们在没有网络的情况下无法连接外网上的SVN仓库,或者没有条件搭建SVN服务,这时我们同样可以使用SVN管理我们自己的代码。我们可以使用TortoiseSVN在本地创建代码仓库,并进行代码的版本管理。具体步骤如下:

  1. 新建一个空文件夹,用于存放本地的代码仓库;
  2. 在这个空文件夹上点击右键 -> TortoiseSVN -> Create Respository here,创建仓库;
  3. 在另一个目录Checkout,本地SVN路径格式类似于:file:///C:Repo

3. SVN命令行操作

在Windows系统中TortoiseSVN是进行SVN代码管理的最佳利器,操作也非常简单方便。但是在一些特殊环境下,熟悉SVN命令行操作也是必须的,譬如想在一些自动化脚本程序中使用SVN的功能。 这里总结一些常用命令如下,更多命令请访问后面的参考链接:

(1) 从版本库获取信息

  • svn help
  • svn info $url
  • svn list
  • svn log
  • svn diff

(2) 从版本库到本地

  • svn [co|checkout] $url $local
  • svn export $url $local
  • svn [up|update]

(3) 从本地到版本库

  • svn import $local $url -m "some comments..."
  • svn add $file
  • svn delete $file
  • svn mv $oldfile $newfile
  • svn rm $url
  • svn [ci|commit]
  • svn revert $file 和 svn revert -R $dir

4. 更换版本比较工具

在进行SVN提交时需要非常谨慎,每次提交之前应先做SVN更新或与资源库同步,要特别注意SVN关于冲突和错误的提示信息。对每个提交的文件进行多次检查,确认它们是不是你真正想要提交的。

在提交的文件上选择Compare with base可以将本地代码和版本库中的代码进行比较,确保修改的内容无误。TortoiseSVN默认使用自带的TortoiseMerge工具进行代码比较,也可以更换其他的代码比较工具:在TortoiseSVN -> Settings -> Diff Viewer选项中找到Configure the program used for comparing different revisions of files,选择External,然后选择你喜欢的比较工具,如:Beyond Compare。

5. SVN服务器迁移

这条其实并不算SVN技巧,但是在我们日常工作中确实经常会遇到这样的情况,想将一台SVN服务器上的仓库迁移到另一台服务器。如果SVN服务器是一台Windows服务器,可以直接将SVN仓库目录复制到新服务器上,然后在新服务器上重启SVN服务即可。如果SVN服务器是linux服务器,可以通过下面的命令操作,将一台服务器上的目录拷贝到另一台服务器。

$ scp -r root@x.x.x.x:/home/svn /home/svn
$ svnserve -d -r /home/svn

在TortoiseSVN客户端,需要更新SVN地址:右键 -> TortoiseSVN -> Relocate...

6. svn:ignore

我们在用SVN提交代码时,常常有一些文件未版本化并且也不想提交,所以在提交时根本不想看到这些文件,譬如类似于Visual Studio工程的bin obj目录。

可以使用 svn propset svn:ignore 命令来将某个文件或目录添加到忽略列表中。可以在下面的链接中找到一些常见的ignore文件:Best general SVN Ignore Pattern?

7. SVN目录结构

Subversion有一个标准的目录结构,如下所示:

svn://projects/
|
+- trunck
+- branches
+- tags

其中,trunk为主开发目录,branches为分支开发目录,tags为tag存档目录(不允许修改)。这几个目录具体怎么使用,svn并没有明确规范,一般有两种方式:trunk作为主开发目录或者trunk作为发布目录。

8. SVN使用原则

  • 代码变更及时提交,避免本地修改后无法修复;
  • 提交前确认代码可编译通过,保证新增的文件同时被提交;
  • 不要将格式修正和代码修改混合提交。修正格式包括增加缩进、减少空格等,如果把这些和代码修改一起提交,很难从日志信息中发现代码修正记录;
  • 每次提交尽量是一个最小粒度的修改,如果一次提交涉及到两个完全不同的功能,那么分两次提交,并在注释中写清楚提交内容;
  • 所有代码文件使用UTF-8格式;
  • 提交的文件必须是开发者公用的程序文件,不要提交私人测试程序、程序缓存、图片缓存、自动生成的文件等。

参考

  1. SVN之使用原则
  2. svn强制写日志和可以修改历史日志(svn钩子的应用)
  3. SVN搭建本地文件版本管理
  4. SVN 命令参考
  5. 关于SVN 目录结构
扫描二维码,在手机上阅读!

Windows+IIS+ASP.NET+SQL SERVER 出现50x错误的解决思路

最近一段时间服务器老是出现500、502或者503错误,由于数据量大了,访问用户增多,这类错误越来越影响正常业务。花了无数的时间排查问题,现做简单总结如下。如果你有新的建议,我会后续补上。 首先需要明确的是出现这类问题肯定是服务器错误,有可能是程序问题,也有可能是系统问题,要根据实际情况进行排查。在我遇到的情形中,我是按下列步骤进行排查的:

1. 首先打开任务管理器,查看内存占用

如果内存占用始终在95%上下,有可能是服务器上某个程序太耗内存导致内存不足。内存问题的排查情况比较多,可能是网站程序有内存泄露,也有可能是某个操作非常吃内存。
在我的服务器上,出现的问题是任务管理器里所有进程占用内存都不多,但是显示内存占用100%,在SQL SERVER中执行下面的SQL,确定是数据库缓存的问题。重启数据库,并为数据库设置最大占用内存可以解决这个问题。

select counter_name, ltrim(cntr_value*1.0/1024/1024)+'G' as memoryGB
from master.sys.dm_os_performance_counters
where counter_name like '%target%server%memory%'
    or counter_name like '%total%memory%'

-- Target Server Memory (KB) 服务器能够使用的动态内存总量。
-- Total Server Memory (KB) 从缓冲池提交的内存 (KB)。注意:这不是 SQL Server 使用的总内存。

2. 查看CPU占用

CPU占用100%同样可能会导致服务器出现50x错误,CPU问题的排查和内存问题一样,需要根据具体进程进行排查。在我的一次排查中,发现sqlservr.exe进程一直占用95%以上的CPU。进入SQL SERVER执行sp_who可以看到连接数很多,而且大多数状态都是suspended。使用下面的SQL语句可以查看当前正在执行的具体SQL。

SELECT
    [Spid] = session_Id,
    ecid,
    [Database] = DB_NAME(sp.dbid),
    [User] = nt_username,
    [Status] = er.status,
    [Wait] = wait_type,
    [Individual Query] = SUBSTRING(qt.text, er.statement_start_offset / 2,
        (CASE WHEN er.statement_end_offset = - 1
              THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
              ELSE er.statement_end_offset
         END - er.statement_start_offset) / 2),
    [Parent Query] = qt.text,
    Program = program_name,
    Hostname,
    nt_domain,
    start_time
FROM sys.dm_exec_requests er
INNER JOIN  sys.sysprocesses sp ON er.session_id = sp.spid
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
WHERE session_Id > 50 /* Ignore system spids.*/ AND session_Id NOT IN (@@SPID)

结果显示正在执行很多相同的SQL,而且都是suspended状态。定位到具体的SQL语句,然后就是优化工作了,为该SQL添加索引后问题解决。

3. 检查IIS应用程序池是否正常运行

有时候IIS的应用程序池会异常退出,也会导致50x错误。应用程序池的退出很大一部分原因都是由于上面两个原因导致,要么是内存爆掉了,要么是CPU爆掉了,从而导致应用程序池崩溃这样的连锁反应。进入IIS->应用程序池,启动相应的程序池问题解决。

4. 检查数据库死锁

通过下面的SQL可以查看设置的死锁超时时间,如果LOCK_TIMEOUT值是-1,意味着如果遇到死锁则进程一直等待资源释放,这样就可能导致50x问题。可以将死锁的超时时间设置为3秒,则可以解决这类问题。具体的死锁原因还需要进一步分析。

SELECT @@LOCK_TIMEOUT
SET LOCK_TIMEOUT 3000

注意:LOCK_TIMEOUT值是程序级的设置(application-level setting),只对当前连接有效。在连接打开后,可以使用SET LOCK_TIMEOUT设置死锁超时时间。不能设置成全局的,如果想设置全局,可以使用CommandTimeout值代替。

参考链接:

1. 查询sqlserver 正在执行的sql语句的详细信息
2.SQL Server 性能调优(cpu)
3. 快速搞懂 SQL Server 的锁定和阻塞
4. SQL Server性能调优:资源管理之内存管理篇(上)

扫描二维码,在手机上阅读!