Memory Dump
A few years ago I wrote a blog entry called J3EE
and defined what I wanted to see in a hypothetical future version of
J2EE. These days J2EE is called JEE apparently but here I want to
upgrade the J3EE idea to a shiny new J3.1EE version number :-) Note
that it has nothing to do with JEE official version numbers.
In that old article I praised Spring as "The most complete pseudo-J3EE
container available right now." But I think these days it can be said
that Spring looks rather "old" compared to other frameworks that came
to further advance the idea. These days it can be said that many things
in Spring are simply "legacy", the blablaTemplate classes for example,
or the TM abstraction. In those good old days, AOP, IoC/DI,
Annotations, and Lightweight configurable enterprise services
were promising a better future for enterprise java development. Here is
an update on their statuses:
AOP
AOP has moved forward quite a bit. The frontier in this space is still
Spring with its fantastic AOP support in form of a powerful AspectJ
based pointcut language and built-in aspects. Plus, aspect definitions
are POJOish now. I don't know about you, but I use AOP, and by AOP I
don't mean lame interceptors like EJB3's. A flexible, but yet
sophisticated framework such as Acegi couldn't have existed without
true AOP.
DI
The funny "to be setter based or constructor based" DI debate has
disappeared. The new debate is on the way we define dependencies.
Personally, I really like the way Google Guice handles it. It's a damn
good use of annotations and in my opinion a very good trade off between
flexibility (Spring) and ease of use (Seam). And it's the only type
safe solution out there.
Annotations
Gavin certainly is the King of annotations :-) JBoss Seam,
Hibernate/EJB3 annotations are some very beautiful usages of
annotations. Compare Hibernate annotations to the old cfg.xml files.
Annotations are cleaner and easier to use definitely. God bless the
guys who popularized annotations ;-) See how Seam's elegant use of
annotations leads to a much easier to use and "modern" framework
compared to Spring WebFlow which looks completely old fashioned!
Lightweight configurable enterprise services
Spring definitely still has the most complete support for this, but
JBoss seems to be closing the gap: Seam's 89MB download bundle is an
evidence of that :-) Besides, JBoss adds some interesting toys to mix,
for example JBPM and its usages in Seam and other places. I'm looking
forward to JEE 6 profiles
and how these profiles would make JEE less monolithic and possibly
provide better plugin points for defining and managing these services.
I would really like to be able to easily add, let's say the JMS profile
to
my websphere deployment whenever I need it. Or a portlets profile, and
so on. I'm also very excited about OSGI and I'm waiting to see how
Spring OSGI will look like. I also would like to see more off the shelf
beans/services exposed via POJOish JMX beans.
J3.1EE :-)
A mix of Spring 2's rich AOP, a type safe and pragmatic DI framework
like Guice, a good dose of annotations in a "modern" framework like
Seam, and even more lightweight and dynamic services (profiles, OSGI,
JMX) seem to be parts of my 3.1 upgrade.
I still am not quite excited about some technologies that have been,
let's say, forcefully added to the mix, such as JSF. But Seam and the
future Web Beans spec seems to fix most of its problems though I just
don't like some parts of the JSF core architecture and with AJAX things
are quite different from the days JSF was designed. Nonetheless, I
believe we definitely need more flexible/customizble JEE building
blocks (profiles) and a much easier programming model to compete
with Rails. I've been working on a startup public site recently and
given the kind of time constraints that startups have I can completely
understand why Rails is such a brilliant infrastructure in such cases.
Ara.
Hola! I’m still alive and I?m back with a new blog post on profiling and a little profiling framework too!
Recently I had to help a team in profiling and optimizing a huge application, which is deployed on a mainframe and has a pretty sophisticated logic. I couldn't use a profiler tool, such as JProfiler. Most of these tools have hooks deep inside the JVM and you?re out of luck when you don?t have enough control on the environment on which the application is deployed. And of course to get a more realistic view of the application, you must profile it under real world loads, so you can?t pause or slow down the application to look around in the app using a profiler tool. And sometimes you need to profile an already deployed application because under some circumstances it performs rather weakly. So you need a tool for metering the performance which could be enabled/disabled dynamically and focused for gathering performance data only for the problem at hand.
And that?s why I created my little profiling framework!
I have used YourKit profiler before. I know you can get on demand snapshots of heap using this tool. I?ve used Atlassian-profiling too, which lets you see a nice call stack of code. It?s useful for development, when you want to see which methods where called and how long each of them took. But it?s for a single call sequence, for example for a single http request. You can?t calculate TPS with it because to calculate it you need to fire up a bunch of concurrent requests and calculate TPS as a whole. And I knew Spring has a StopWatch class, and an interceptor which wraps beans and uses this stop watch, but nothing more than that.
So I created a very simple framework for profiling "execution speed" in a very flexible and configurable manner. It does nothing for memory profiling.
It consists of a StackedStopWatch class and has an interface similar to Atlassian-profiling:
StackedStopWatch.start(?deposit?);
try {
StackedStopWatch.start(?depositValidation?);
//deposit validation code here
StackedStopWatch.stop(?depositValidation?);
//deposit code here...
}
finally {
StackedStopWatch.stop(?deposit?);
}
Under the hood, StackedStopWatch creates StopWatchExecution objects which collect performance data for stop watches. Like atlassian-profiling it keeps a tree of StopWatchExecution objects, so later I can print out a nice call stack of all calls, but unlike atlassian-profiling it's logged using commons-logging instead of System.out.println() on the console. So, if I enable logging for "StackedStopWatch", "deposit" and "depositValidation" let's say in log4j's log4j.properties (or in case of JDK 1.4 logging in logging.properties), I get an output like this in my logging file or console or on a socket or wherever I've configured logging to log to:
[571ms] - deposit
[123ms] - depositValidation
There's also a servlet filter called ProfilingFilter which can be used to log http request/response performance. And also an interceptor I can configure to automatically log method calls:
<bean id="performanceMontitor" class="org.springframework.util.perf.SmartPerformanceMonitorInterceptor">
<property name="useDynamicLogger" value="true"/>
</bean>
<aop:config>
<aop:pointcut id="allMethods" expression="execution(* *..*.*(..))"/>
<aop:advisor advice-ref="performanceMontitor" pointcut-ref="allMethods"/>
</aop:config>
Here I attached this interceptor to all methods of all beans "but it logs only calls which are configured to be logged". I can enable logging for everything except requests for /img/* and /attachment/* urls and some DAO class named com.myapplication.dao.HibernateReportGeneratorDao, as below:
com.myapplication.level=FINEST
org.springframework.util.perf.web.ProfilingFilter.img.level=OFF
org.springframework.util.perf.web.ProfilingFilter.attachment.level=OFF
com.myapplication.dao.HibernateReportGeneratorDao.level=OFF
And then I get a log like this for a request to /some/url:
[1733ms] - org.springframework.util.perf.web.ProfilingFilter.some.url
[1207ms] - com.myapplication.web.DepositController.handleRequest
[199ms] - com.myapplication.dao.HibernateDepositDao.findDepositData
[180ms] - com.myapplication.dao.HibernateUserDao.findUser
This is pretty useful during development. For profiling a deployed application we need to put it under real load and collect detailed profiling data once in a while periodically and analyze these logs. I've written a JMX bean for this purpose. It only has one method which when called logs a table like this:
------------------------------------------------------------------------------------------------
s % # tps Task name
------------------------------------------------------------------------------------------------
00000.1 000% 00001 00012.5 com.myapplication.security.entity.dao.HibernateRoleDao.getAll
00000.1 000% 00012 00011.1 com.myapplication.entity.dao.HibernateTagDao.getAll
00000.1 001% 00001 00010.0 com.myapplication.entity.dao.HibernateTagDao.findPrimaryTags
00000.2 001% 00001 00005.9 com.myapplication.security.entity.dao.HibernateUserDao.getAll
00000.2 001% 00001 00005.6 com.myapplication.entity.dao.HibernateTagDao.findPopularTags
00000.2 001% 00001 00005.5 com.myapplication.entity.dao.HibernateTagDao.getAverageRelatedTagsFor
00000.3 001% 00001 00003.7 com.myapplication.entity.dao.HibernateTalkDao.getAll
00000.3 002% 00001 00002.9 com.myapplication.bucket.entity.dao.hibernate.HibernateEntityObjectDao.getAll
00000.6 003% 00005 00001.6 org.springframework.util.perf.web.ProfilingFilter.user.ara.largephoto
00000.7 004% 00002 00002.8 com.myapplication.security.entity.dao.HibernateUserDao.findUserByUsername
00000.8 004% 00001 00001.3 org.springframework.util.perf.web.ProfilingFilter.user.armond.largephoto
00002.0 010% 00015 00000.5 com.myapplication.entity.dao.HibernateTalkDao.findTalksForTag
00002.1 011% 00020 00000.5 com.myapplication.web.talk.ViewTagController.handleRequest
00017.3 088% 00020 00000.1 org.springframework.util.perf.web.ProfilingFilter.some.url
This data is generated from all the StopWatchExecutions collected up until now. You can see total seconds, percentage of each call compared to the total, how many times each one was called and a TPS for each one.
Normally someone calls you and complains about some feature of the application being slow. You enable loggings, analyze the logs, disable some logs for focusing on the logs related for the problem at hand, play with the configuration or the code to boost up performance, and end up with logs that show this performance improvement.
Sometimes you need to see the arguments a specific method was called with. You can enable logging more information:
more.com.myapplication.entity.dao.HibernateTalkDao.findTalkByEscapedSubject.level=FINEST
And it logs arguments too:
[1733ms] - org.springframework.util.perf.web.ProfilingFilter.some.other.url
[1207ms] - com.myapplication.web.DepositController.handleRequest
[199ms] - com.myapplication.entity.dao.HibernateUserDao.findUser
(ara)
(joe)
Because I used logging in this framework, you can dynamically enable/disable logging and change logging levels, for example by using JDK's JConsole utility. So whenever you want to profile something, just fire up jconsole, enable logging for a piece of code, collect detailed logging data, analyze it, tune the configuration or change the code, and hopefully if that change doesn't force you to restart the server just disable logging and let the users enjoy the improved performance!
And I put this code in org.springframework.util.perf package 'cause I think it must be built into Spring :) You can download the code from here. Let me know if it's useful and then we'll find a way to either contribute it to an existing open source project or host it somewhere else.... It certainly could be enhanced to support more sophisticated profiling scenarios.
Ara.
Recently I was involved in a workflow-based project. The project uses
OSWorkflow heavily and has various sophisticated workflows. What I noticed from
looking at the workflow related code was how ugly and complicated the code is,
or becomes over time. It's because of the implicit programming model of these
workflow frameworks (or whatever you call them: graph oriented programming, bla
bla). So the xml file defining the workflow has lots of get/set("thisorthat") of
properties attached to a workflow instance like this:
<result id="412" due-date="" old-status="Finished" status="Queued" step="3">
<conditions>
<condition type="beanshell">
<arg name="script"><![CDATA[
propertySet.getString("check.inventory.result").equals("none")
]]></arg>
</condition>
</conditions>
<post-functions>
<function type="class">
<arg name="order.new.status.id">NOT_AVAILABLE</arg>
<arg name="order.old.status.id">ORDER_APPROVED</arg>
<arg name="service.name">OrderBizLogicService</arg>
<arg name="class.name">util.osworkflow.BizLogicServiceFunctionProvider</arg>
<arg name="method.name">persistOrderStatus</arg>
</function>
</post-functions>
</result>
This is a step of the workflow which checks whether a property has been set
to "none" and triggers a persistOrderStatus method in a bean to do something
with the workflow, like setting some more properties and saving some stuff in
the database. The code for the bean looks like this:
public class OrderBizLogicServiceBean
extends BizLogicServiceBean {
...
public void execute(Map transientVars, Map args, PropertySet ps)
throws ServiceException {
String methodName = (String) args.get(METHOD_NAME);
WorkflowEntry workflowEntry = (WorkflowEntry) transientVars.get(WORKFLOW_ENTRY);
long workflowId = workflowEntry.getId();
if ("persistOrder".equals(methodName)) {
/* gets ORDER_HEADER from transientVars */
OrderHeaderValue orderHeaderVO = (OrderHeaderValue) transientVars.get(ORDER_HEADER);
/* gets ORDER_ITEMS from transientVars */
OrderItemValue[] orderItemVOs = (OrderItemValue[]) transientVars.get(ORDER_ITEMS);
/* gets ORDER_ROLES from transientVars */
OrderRoleValue[] orderRoleVOs = (OrderRoleValue[]) transientVars.get(ORDER_ROLES);
/* gets USAGE_ID from transientVars */
String usageId = (String) transientVars.get(USAGE_ID);
/* invoke persistOrder() method */
String orderId = persistOrder(orderHeaderVO, orderItemVOs, orderRoleVOs, workflowId, usageId);
/* put ID of newly created OrderHeader in PropertySet */
ps.setString(ORDER_ID, orderId);
/* Do some DAO calls here.... */
/* calls persistOrderStatus() method */
} else if ("persistOrderStatus".equals(methodName)) {...}
}
It's hell of an ugly code, all those get/set calls to get or put some stuff attached to this instance of workflow is very ugly, unreadable and error prone. As far as I've seen, this style of "implicit" coding is the norm in all workflow/graph/bpm/etc frameowrks of this kind. Some properties are attached to the workflow in some step of the execution, somewhere else some of those properties are fetched, some juggling is done and yet some more properties with some literal names are attached to the workflow instance which will be later used in some other step or substep of the flow. You can imagine how easily this style of coding can lead to disaster. It's hard to trace what's there in the workflow instance, what has happened so far. There's no one to one mapping from the workflow definition file to any piece of business logic code which deals with the workflow. Everything is implicit, get this literal, set that, no compile time help of any kind. What if you misspell one of those properties or the method name or ...? And the whole workflow API is directly exposed to your business logic code, which is bad, because business logic must be only business logic not low level workflow API calls in obscure if/else checks. It's all implicit. Implicit code is flexible but hard to maintain. It all leads to unreadable and ugly code. Hell no! Not my code! So I came up with a different, a more explicit, workflow programming model, based on what I call POWOs (plain old workflow objects) or workflow beans. Yes guys! Yet another POxO!! In this model I tried to make things more explicit, with no calls to any workflow API in the business logic code. So for each workflow definition file, there's a "code-behind" workflow bean or POWO. It's like jsp/asp really. There's the tag and you know there's a bean mapped to that tag. It's obviously easier to track:
<action id="1" name="Start Workflow"> <pre-functions> <function type="workflowbeanshell"> <arg name="script"><![CDATA[ workflowBean.setPrOrderId("666") ]]></arg> </function> <function type="workflowbeanshell"> <arg name="script"><![CDATA[ workflowBean.persistOrderStatus("NOT_AVAILABLE", "ORDER_APPROVED") ]]></arg> </function> </pre-functions> </action>
In the code above, instead of explicitly using propertySet.setString() we call a setter method on the powo bean of this workflow. persistOrderStatus() is also defined in that powo and we simply call that method. The powo itself looks like this:
public class SampleWorkflowBean extends WorkflowBean
{
private Object workflowManager;
private String prOrderId;
private String trSomething;
public void setWorkflowManager(Object workflowManager) {
this.workflowManager = workflowManager;
} /** use "Pr" prefix for all attributes of Propertyset */ public void setPrOrderId(String prOrderId) { this</B>.prOrderId = prOrderId; } public String getPrOrderId() { return prOrderId; } /** use "Tr" prefix for all attributes of TransientVars */ public String getTrSomething() { return trSomething; } public void setTrSomething(String trSomething) { this.trSomething = trSomething; } /** A function called from within workflow xml file "Sample.xml" */ public void persistOrderStatus(String orderNewStatusId, String orderOldStatusId) { setPrOrderId("123"); //save the order, do whatever... //and to work with a transientVar instead of this: //String something = (String) transientVars.get("something"); //do like this: String something = getTrSomething(); //this is the id of the workflow we're operating on: long workflowId = getWorkflowId(); //workflowManager is dependency injected from Spring context xml file: workflowManager.createWorkflowReference("OrderHeader", getPrOrderId(), workflowId); } } Looks like a pojo! There's no transientVars, propretySet or WorkflowEntry or anything like that. Somehow magically all those getter/setter methods are intercepted (using Spring) and delegated to the proper propertySet/etc object:
public class AttributeFlusherInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
String methodName = method.getName();
Object rval = null;
if(methodName.startsWith("setTr")) {
String capAttributeName = methodName.substring(5);
String attributeName = StringUtils.uncapitalize(capAttributeName);
WorkflowContext.getWorkflowContext().getTransientVars().put(attributeName, invocation.getArguments()[0]);
} else if(methodName.startsWith("getTr")) {
String capAttributeName = methodName.substring(5);
String attributeName = StringUtils.uncapitalize(capAttributeName);
rval = WorkflowContext.getWorkflowContext().getTransientVars().get(attributeName);
invocation.getThis().getClass().getMethod("setTr" + capAttributeName, new Class[]{rval.getClass()}).invoke(invocation.getThis(), new Object[]{rval});
} else if(methodName.startsWith("setPr")) {
String capAttributeName = methodName.substring(5);
String attributeName = StringUtils.uncapitalize(capAttributeName);
WorkflowContext.getWorkflowContext().getPropertySet().setAsActualType(attributeName, invocation.getArguments()[0]);
} else if(methodName.startsWith("getPr")) {
String capAttributeName = methodName.substring(5);
String attributeName = StringUtils.uncapitalize(capAttributeName);
rval = WorkflowContext.getWorkflowContext().getPropertySet().getAsActualType(attributeName);
invocation.getThis().getClass().getMethod("setPr" + capAttributeName, new Class[]{rval.getClass()}).invoke(invocation.getThis(), new Object[]{rval});
}
rval = invocation.proceed();
return rval;
}
}
The code for creating a new workflow instance and calling an action of it is as simple as this:
SampleWorkflowBean sampleWorkflowBean = (SampleWorkflowBean) springAppContext.getBean("sampleWorkflowBean");
SampleWorkflowBean bean = (SampleWorkflowBean) sampleWorkflowBean.create();
bean.doAction("11");doAction() and create() are two of the methods defined in the WorkflowBean base class. The powo derives from WorkflowBean but the powo is a pure pojo. So in the end for every workflow there's a corresponding powo for it, and it's as clean as possible. By looking at the powo we see a list of all the properties of a workflow (as bean accessors), and they are accessed only via this powo with no sloppy get("thisorthat"). This model has a few fantastic side-effects too. For example, if you have 2 workflows both dealing with orders, you define a base class for both and move the common properties up to that base powo! It encourages clean hierarchies and reuse. There are a few other classes involved too, but I won't describe them here, this post is already too long! I wonder if anyone else finds this way of working with workflows interesting? I may be able to opensource the relevant parts.
So here is my report from the first day of JavaPolis 2004 :-) This is my first
time here in Antwerp.
I went to Adrian Colyer's AspectJ in Action presentation and then to Joshua
Bloch and Neal Gafter's JDK 5.0 in Action talk. They were both interesting, even
though I was familiar, to some extent, with both topics.
What caught my attention from the AspectJ talk was the idea of Architecture
enforcement. Basically it means using "declare warning" to define an aspect for
showing warning messages when architecture forbids something and some piece of
code coded by a silly programmer insists on breaking law and order :-) So for
example you could use it to enforce your team members to never ever use
System.out.println() or never ever do UI logic from within DAO classes. Stuff
like that. Good idea for big teams. Not a really useful thing for teams of rock
star developers. It's interesting that turning on/off aspect "libraries" is just
a matter of including/excluding the jars from the runtime of the application.
Compared to Spring/AspectWerks/others, I think AspectJ's approach is more
refactoring-oriented than lets-code-some-aspects-from-the-begining of Spring and
the gang. It's much easier to refactor existing code to an aspect in
Eclipse+AspectJ than say in Spring. Of course Eclipse's built-in AspectJ support
for such refactoring plays a central role in this "overall approach" to coding.
The syntax is ugly but powerful. And I can't imagine how Tiger's metadata stuff
could make it more bearable, because I think at least for pointcut declaration
is doesn't make much sense and doesn't improve anything in any way!
Btw I liked the analogy Adrian made from pointcuts to commentary of a football
match :-) And I liked his English accent too, classy :-) And according to him
the holy trio of software at the moment are: AOP, DI (Dependency Injection) and
my good old friend Annotations.
The JDK 1.5 talk was an overview of the new language stuff of the Tiger release,
plus some recommendations on when to use and not to use some of those stuff.
Joshua and Neal are fantastic speakers, specially when they are pair-presenting
on stage! I used the 30 minutes coffee break to talk to Joshua about the
annotation stuff, and although he was quite visibly exhausted but he answered my
questions in great detail and accuracy. I guess now I understand some of those
compromises made by JSR-175 expert group. They make sense. And some of those are
obviously postponed to later releases. I expressed my concerns about some those
compromises/limitations in a previous blog entry. Josh told me about a new tool
in JDK 1.5 called apt. I think turning apt into a JSR is a very good idea. We
could then hook our "annotation validation" stuff to it and have a complete
solution for validating annotations. Validation was one of my concerns/complains
about this spec. Another use would be "annotation overriding", to override some
annotations with some others defined in an xml file for example (which I beleive
EJB-3 group wants to do too).
Overall was a fantastic day! Though I didn't meet any of my friends today :-(
Ara.
Ever wanted Dependency Injection for your unit tests? I got sick of doing context.getBean("thisDao") and context.getBean("thatDao") when testing my
springified DAO classes.
What I wanted was something like this:
public class TestUserManagement extends BaseTestCase {
private UserDao userDao = null;
public void testSomething() throws Exception {
userDao.createAdmin();
}
}
Note that I don't new any UserDao. I expect the DI container to instantiate it for
me.
As far as I know there's no DI container for junit, so I created one! But
because I'm a lazy person and laziness is virtue I ended up with a very simple
implementation. Here is the code:
public class BaseTestCase extends TestCase {
protected ApplicationContext context;
protected void setUp() throws Exception {
context = new ClassPathXmlApplicationContext(new String[]{
"/testApplicationContext.xml",
"/otherContext.xml"});
initDependencies();
}
private void initDependencies() {
Field[] fields = this.getClass().getFields();
for (int i = 0; i
In setUp() the Spring context is created and then dependencies are retrieved
from the context and set. Instead of doing constructor or setter injection, I do
field injection, with a trick. The code loops over all fields and if there's a
bean defined with the name of the field, it sets it in the test class.
So in case of our sample test case above, making the field public like this:
public UserDao userDao = null;
And defining the dependency in the context xml file with the bean name "userDao"
does the job. The field has to be public otherwise security manager complains
about malicious code trying to hack our class by changing the field value :-)
So now I'm a happier guy because I don't have to write any bean lookup code
in my test cases :-)
And in case you're wondering "what about mocks?", well mocks are ok, but you
can't use mocks when you want to test the real code or when you're doing
acceptance tests and such.
Ara.
Read
an interesting statistical analysis of the 2004 US elections by the
Washington Monthly magazine and "Why the next election won't be close". Also
according to the article John Kerry has a good chance of winning it too, and the
other way around :-)
Ara.
I usually keep a low tone on my work on this weblog, but this posting is
different because I and My friend Armond (and a few others) are starting our own
company!
So the idea is to offer offshore development, with excellent quality and
excellent prices. Our team has a very successful track record, I think :-)
Confluence,
Java Open Source Programming book and
XDoclet (admittedly long time ago) are a few of our works, the ones you've
probably heard of. We've been involved in various kinds of projects, hardcore
enterprise projects (Websphere, AS400, etc), web based thin clients,
sophisticated Swing fat clients, mobile projects and so on!
So if you have a project and you're interested email me at ara_e_w at yahoo
dot com.
Ara.
J2EE is like cooking at your own kitchen. Delicious but you have to learn
cooking :-)
.Net is like buying fast food. Fast in, fast out, but is it healthy? Do you
know what you just ate? Where's the sauce? :-)
Hope you don't have kitchen-phobia ;-)
Ara.
Hereby I announce the winner of the most descriptive error message
competition (encountered by Farzad):
"[ERROR] java.sql.SQLException: [PWS0001] Function did not complete
successfully. Cause . . . . . : An error occurred while running the function
which caused the function to end before it could be completed. Recovery . . . :
Determine what caused the requested function to fail, correct the problem, and
run the function again."
Reported by WebSphere on AS400 from a DB2 database. Viva IBM!
Ara.
I find this post by Kathy
Sierra on Pair Programming and loners very interesting.
Being alone simply gives you the time to reorganize your thought. Pair
Programming on the other hand achieves the same goal by exchanging thoughts
among partners. I feel like doing the loner's way sometimes (most of the time?)
and sometimes just by pairing with a colleague. People are different, they are
in different moods, they organize their minds quite differently....
Related to this issue, I would really like to see a study on differences
between male and female programmers 'cause I've seen some big differences on how
each group analyze information. Does anyone know of such a study?
Ara.
Newsfeed display by CaRP |