Pathfinder Blog
Archive: April 2006

Ajax: The “Husky” Client

Scott Dietzen over at Zimbra has a post in a continuing series on AJAX scalability. Besides coining the humorous term "Husky" Client to describe AJAX -- not quite thin, but not quite fat -- he makes some excellent points about the importance of design and choosing the appropriate browser/server boundary for an application in order to minimize the impact on the server.

I thought the following early paragraph was a nice observation by someone who clearly has a bit of experience developing applications:

Traditional fat client applications, on the other hand, off-load all of
the UI and most of the business logic (modulo stored procedures and
triggers) from the server to the client. Fat client app's could
nevertheless hammer their servers simply by not being sophisticated
about how much and how often data was being requested---that is, data shipping to the client can be more expensive than function shipping
to the server (with stored procedures, triggers, et al). With a
reasonably smart design, however, fat client applications typically use
more client and less server CPU per operation than a corresponding
server-centric application.

I think the generation of RIA's (Rich Interaction Application) that are about to sweep the web are likely going to repeat many of the mistakes of the client/server age. As Scott points out, how poorly these applications perform is going to be in part dependent on how well they are designed. As Santayana wrote, "Those who cannot remember the past are condemned to repeat it."

It Just Keeps on Flowing

Our Elyse Sanchez has just authored an article entitled "Features into Flow: Techniques for Optimizing User Interactions." It distills some of our experiences designing software for educational reform in an Arabic gulf state. Some choice excerpts:

The success of our design methodology is dependent in a large part on user research and the creation of personas and task-based scenarios. Our principle Enumeration persona, Ahmad, spends a few weeks each autumn traveling through the desert from school to school, generally spending about a week at each location, where he has to manage with makeshift accommodations and associated distractions. A thorough and competent data collector, he dislikes making errors. Using his laptop computer, he enters his data very quickly but often struggles to meet his deadlines.

We were able to incorporate several strategies for optimizing flow and offering Ahmad the benefits of a “smart” application. First, we designed a linear, tabbed structure that guided the information-collecting process and ensured that all dependencies were correctly met (for example, information on teachers had to be obtained and completed before student information could be gathered). For selected subtasks, information had to be moved from one area of the display to another. To enable this in as few keystrokes as possible, we implemented a drag-and-drop function that greatly simplified what could have been monotonous, time-consuming data-recording tasks.

You can read the article here (pdf).

Types of Flow - A Quick Reference

How does the concept of flow manifest in an application? We usually look at three different aspects of flow during the design process.

The first is efficiency. Often we are seeking the use of an application to save us time in accomplishing what we have to do. In the user’s eyes, efficiency gains are often tied to satisfaction and are frequently where flow starts.

The second aspect of flow is hybrid navigation. Most applications of moderate or greater complexity will require a combination of linear and non-linear activities. It is important that the designer be able to empower each activity while keeping the overall scheme consistent and intuitive.

The third aspect of flow is comfort. When the interaction with a product is comfortable and natural, the product starts working at an emotional level. In technology-based products, accomplishing comfort in the design can be a significant differentiator in the marketplace.

Ajax and Leaky Business Logic

I gave my presentation last evening to a welcoming Chi2 audience. The attendees were a good mixture of developers, designers and business folks. They sat through my rantings on why I thought Rich Interaction Applications (RIA) in AJAX should be developed with server-side component based GUI's, though, in a pinch, I could see the argument for client-side component based GUI's that use the back-end as a dumb data web service. Then I showed them what 150 lines of Java code could do in the Echo2 framework with my special "why would you do things this way" stock quote app. Lot's of dragging and dropping ensued.

I'll put the presentation and the demo up when I get a chance.

One of the more interesting and probing questions after the talk was about business logic leakage, i.e. how does a server-side component GUI (SGUI) keep you from leaking business logic? The answer is simple: with the SGUI, the only "logic" that makes it to the browser is "turn this area blue," "move this button over here," and "make this element draggable." While I suppose it is possible to leak business logic in this way, I'd have to say that the possibility is pretty remote and esoteric, akin to sending smoke signals.

In the pseudo hand-coded world of traditional webapp development, you are dependent on developers following good practices and not making mistakes, such as leaking the secret sauce in their Javascript validation logic. At least you hope they do. If the last 50 years of software development practice have taught us anything, it's that hope is not a plan.

There are two kinds of information leak -- what and how. "What" is your customer data, your orders, your accounts receivable, your employee database. The "how" is your secret sauce, the special way of pricing or quoting or doing that gives you a competitive advantage. Reports can leak the "what," and many businesses can survive a leak of quite a bit of "what," but leak the "how," your secret sauce that makes you money, and you're cooked. Javascript can leak the "how" if you let it.

Michael Mahemoff feels it's sometimes OK to include business logic in the Javascript:

I realise there are plenty of problems with Javascript business
logic, and it’s certainly not always applicable. Those problems include:

  • Security - Exposes business logic algorithms and enables them to be changed.

That first bit applies to most all valuable services that a company might provide. If all you're providing is data, then no business logic would be leaked. If you are performing some sort of business logic on the data, it would have to be something pretty trivial to still have value and not be a divulging of the crown jewels. Can a business consist of a bunch of ultra trivial, obvious logic and still be competitive? Probably not. In short, I can't imagine the circumstances where that first item wouldn't just preclude exposing most any business logic.

Towards a concept of flow–UXD perspectives

"Flow" is an attribute we're talking about--and, hopefully, designing towards--here at Pathfinder. As application designers, we've been captive eyewitnesses to the proliferation of contradictory and unrelated features that consititues progress in IA and development.

Design theories of flow are derived, directly or indirectly, from the work of psychology professor Mihaly Csikszentmihalyi, who has dedicated over two decades to developing a theory of "optimal experience."

We've recently shared some of our thoughts on flow at the IA Summit and World Usability Day. Other perspectives include those of Kathy Sierra, the academically focused ACM, and designer/blogger Scott Berkun.

Plus, an interview with The Man Himself in Wired, titled (you guessed it) "Go with the Flow."

UML Modeling in the Browser

There have been some interesting experiments in breaking the bounds of the rectalinear forms-and-reports browser interface. One example,  John Resig's dModeler, is a cut at a UML class modeler. In his words it is:

A web-based UML
modeling program that my team wrote for the 4th Red Bull Programming
competition. We weren’t able to complete all that much of it, but we
did get creation of classes, links, drawing, signups, logins, and
parameter creation finished. At some point it may be fun to sit down
and modify what we wrote and get it fully operational and possibly
release it.

There's no AJAX logic behind it to provide any logic or even persist it, but I encourage folks to look at it that you can provide rich client functionality in the browser. Think outside the grid.

Update 1: you may need to use Firefox to get this to work. Ironically, it was designed to work with IE, but the login has some Javascript errors. Firefox will blissfully ignore the logins and get you to the diagram.

Forward versus Backward Chaining

As if choosing between different BRE vendors wasn't tough enough, there's an added consideration: is the problem you are trying to solve more amenable to a forward or backward chaining approach? Forward chaining, which is the default behavior of most commercial and open-source rule engines, is described as

An inference engine using forward chaining searches the inference rules until it finds one where the If clause is known to be true. When found it can conclude, or infer, the Then clause, resulting in the addition of new information to its dataset.

while backward chaining is described as

An inference engine using backward chaining would search the inference rules until it finds one which has a Then clause that matches a desired goal. If the If
clause of that inference rule is not known to be true, then it is added
to the list of goals (in order for your goal to be confirmed you must
also provide data that confirms this new rule).

So in other words one approach starts with some facts and applies rules to find all possible conclusions, the other starts with the desired conclusion(s) and works backwards to find supporting facts. You can sort of view these approaches as two variations on search, with each step forward or backward forming a tree, either spanning out forwards towards conclusions or spanning out backwards towards initial facts. In point of fact, most rule systems don't go about finding all possible branches in this tree but use some sort of planning or goal setting to guide their search.

What are the advantages of one approach over the other and when would you use each? As it turns out, they can both solve the same problems. Most computer science students have to show at one point in their studies that any backward chaining rule system can be rewritten as an equivalent forward chaining system. Showing the reverse isn't quite so easy, but can be done in a graduate course. So both forward and backward chaining approaches can solve the same problems, but they have somewhat different performance characteristics, and certain problems are much easier to express in one or the other. Charles Forgy put it very succinctly in one of his articles on backward versus forward chaining (sorry, no link. The articles don't appear to be available now that Rulespower has been acquired by Fair Isaac)

You might ask why you would want to use a forward chaining system if you have to write rules to manage subgoals. After all, backward chaining systems automatically manage the subgoals. There answer is very simple: Backward chaining systems are more limited than forward chaining systems. There are many kinds of tasks that can be handled easily with a forward chaining system that are either difficult or impossible with a backward chaining system. Backward chaining systems are good for diagnostic and classification tasks, but they are not good for planning, design, process monitoring, and quite a few other tasks. Forward chaining systems can handle all these tasks.

So, if you already know what you are looking for, e.g. a customer that might be committing fraud, a patient at risk for breast cancer, etc., then backward chaining may be a good solution. On the other hand, if you don't necessarily know the final state of your solution, e.g. improvements to a business process, suggesting next steps in a due diligence investigation, or directing data transformations in an ETL process, then a forward chaining approach my be preferrable.

On the design front, backward chaining product will often have better justification or explanation mechanisms, i.e. explanations on how you arrived at a particular goal. If this is important to you, as it might be if you are working in clinical medicine, then a backward chaining solution might be a good fit.

On the performance front, there are certain circumstances where backward chaining might be better. For instance, if you have a small number of rules and a huge number of facts, you might be able to lazy load only those facts that are relevant to fulfilling goals.

If you'd like to play around a bit with a backward chaining rule engine, give Mandarax a try. Also, some commercial BRE's provide backward chaining as well as some other rule execution algorithms.

Feeling pretty good about your grasp of backward and forward chaining? Did you know there are hybrid approaches that combine both backward and forward chaining? Yep, it's true. But we won't go there today.

March of the Games

It seems the evolution of games that happened on the PC and on-line is repeating itself in the AJAX arena. First come the board games -- see Ajax Chess, 64Pola, and Morfiks -- and the muds -- hive7, then the low fidelity arcade games, then the first person shooter games. Is AJAX Doom too far away? Yeah, probably.

The chess games are actually pretty lousy, more experiments in Ajax rather than real apps. I've written a few chess games in my time and will take a crack at one that incorporates back-end access to a chess engine. Why reinvent the wheel? Just expose a FEN web service wrapper over a winboard engine like crafty.

The truth is that for the first person shooters and other higher perf stuff, flash is better than AJAX based apps are likely to be (See here for everything from Doom to WarCraft to Mortal Kombat in Flash). In truth, if you scratch below the surface a bit, what differentiates something like Flash and Java Applets from AJAX for any sort of application is the install and startup overhead. OpenLaszlo can communicate with the server just fine, thank you, and Java Applets have been able to do that since the beginning. So why all the hype over AJAX now?

Now that it's all wrapped into the browser, it seems tempting to move to AJAX. It's already in there, so they can't resist like they can with Flash and Applets. What are they going to do, turn off Javascript?

But for everyone from those developing public facing to intranet apps, there's reason to look beyond the buzz of AJAX. If you're making the move to interactive, rich client user experience, is AJAX really the right choice? Do you trust your Ajax framework vendor enough or do you have deep enough pockets to keep your apps up-to-date with the changing browser landscape? Is the kind of rich client experience you are after achievable with AJAX? Why didn't you go with the other technologies in the first place and settled on a forms-and-reports conventional webapp instead?

Go over your reasons again or you may find yourself implementing Doom in AJAX.

Update 1: pitstreet.com has an assortment of Javascript games, including some that employ AJAX. Their usability leaves something to be desired. Just try to find the chess or reversi games.

 

Again on Scalability

Since my rant started flowing, I just thought I'd pull this out of the comments section at Ajaxian.com:

Beyond becoming multi-threaded, AJAX is beginning to open up
the web to new types of applications, ones that are document centric
(word processors, modeling tools, etc.) rather than data and
transaction centric, i.e. all those rectangular CRUD applications that
make up 99.9% of webapps. It also means that the sorts and types of
rich client interactions are going to dwarf the traffic that we see
today.

That means 1. abandoning the forms-and-reports way of
writing webapps (which will break when you try to write something like
rational rose as a webapp) and moving to the component GUI model (like
Swing, Winforms, etc.) and 2. being very clever about the frequency and
size of your XHR conversations with the server. From my unscientific
tests (Yahoo mail and Google calendar)
it seems that some are winning and some are losing the battle on fat
XHR. I don’t think any amount of JSON or compressed XML magic will
solve the problem of poor design.

I think the right way to
achieve all of this is by moving AJAX/Webapp development to component
GUI application frameworks. Properly done, they have the potential to
hide all of the messy bits like exposing too much of your business
logic on the client side, optimizing XHR requests for components that
have empty server-side event listeners, reducing the impedence mismatch
between the Javascript/CSS/XHTML world and the business logic.

Those
who don’t move in this direction will be stuck building and maintaining
ever more complex applications because they didn’t make the shift to a
new design. It’s time to think of the browser simply as a display server.

X Windows and Ajax: Two Kinds of Display Servers

X11 is a client/server display system that realizes user interfaces by sending asynchronous messages back and forth between a display server -- that manages a display, mouse and keyboard -- and clients such as xterms, Firefox, GIMP, etc. Some of the drawing, windowing, events and other operations that the X protocol supports look suspiciously like some of the things flying around in Ajax these days.

Now there are some substantial differences between X and Ajax. For one, X clients (the equivalent of the http server) open up persistent connections to the X server (the equivalent to the browser), while the 'Ajax server", aka the browser, has to poll the "Ajax clients" (the apps on the http server) for messages. X is typically used in a LAN rather than a WAN setting, for this reason. Persistent connections usually don't hold up very well in a WAN context. Also, there's no embedded scripting language like Javascript in X.

One definite similarity is that both X and Ajax don't have any widget standards. In X you've got several different widget sets -- Motif, Qt, Gtk, etc. -- while in Ajax you've got a few early frameworks with their own widgets.

So, what's to be learned from all of this? First, while user interfaces may well head in the component GUI/widget directions, we'll likely have quite a number of different widget sets in the long run. Second, there are a few design lessons to be learned from X Windows. For example, the creation of windows with a specific look and feel is controlled by a window manager abstract factory.

Some of these design solutions may not be appropriate for Ajax, but I think it's worth mining this trove of design solutions. Might we some day have an Ajax desktop that unifies multiple independent Ajax applications?

Update 1: Bill Scott makes a similar point (though much earlier -- more coffee, I'm sure).

Maximizing Meetings:

or 6 blind men and an elephant, in a phone conference..

We had a meeting a few days ago with a senior rep at one of our clients. This client was a trainer for the product we are doing some substantial revisions on - we learned more about it’s capabilities and the histories of use in 20 minutes from him than we could have in months of studying the app.

So what do you do with that information once you have it? What is the best way to cut that iceberg of information into usable pieces ?  Iceberg is the right term, because most of it is still hidden.

We decided to separate & each write our own version of the truth. Then to reconvene and compare/contrast/collect the ideas we heard while brainstorming more. This way we each define our individual perceptions, without the dynamics of personality minimizing contributions. We have tried this numerous times & found it highly effective for maximizing creative ideation.

And it is worth noting that information gathering at this phase is anything but linear. There are snippets of narrative, hard parameters that must be recognized, ideas that immediately strike you as being part of a larger solution. Capturing these, sorting them and keeping them available to the team is an ongoing challenge to which there are both simple & complex solutions. These can range from the utility of having a well designed directory with ongoing and meta class folders to full CMS solutions that enable sophisticated search. The solution most useful is determined by utility, cost & scale.

Jetty 6’s Continuation Mechanism for Ajax

I've touched on the topic of updates and asynchronous processing before. My preferred method of performing updates between the browser and server is via a polling mechanism that returns quickly. An alternative is to open up an XHR connection and keep it open to wait for a response (or a timeout). I don't like this method because it is wasteful in terms of sockets and threads; also, it is likely to stress stateful firewalls, load balancers, etc., and may break in lots of client environments.

Nevertheless, if you want to keep a connection open for notification initiated by the server, this is the way for now. And the Jetty 6 server has at least addressed the thread issue with Continuations.

Behind the scenes, Jetty has to be a bit sneaky to work around Java
and the Servlet specification as there is no mechanism in Java to
suspend a thread and then resume it later. The first time the request
handler calls continuation.getEvent(timeoutMS) a RetryReqeuest runtime
exception is thrown. This exception propogates out of all the request
handling code and is caught by Jetty and handled specially. Instead of
producing an error response, Jetty places the request on a timeout
queue and returns the thread to the thread pool.

When the timeout expires, or if another thread calls
continuation.resume(event) then the request is retried. This time, when
continuation.getEvent(timeoutMS) is called, either the event is
returned or null is returned to indicate a timeout. The request handler
then produces a response as it normally would.

Sockets are still consumed, though. Hopefully the next servlet specification will address some of these issues. Until that time, this may be a good workaround.

Still, my preference is to keep everything except for the display logic on the server side, and that includes handling complex communication with async processing.

Update 1: ActiveMQ can make use of Jetty 6's continuation mechanism.

Update 2: Greg Wilkins has some more extensive thoughts on using Jetty 6 to scale Ajax apps.

Webtop - I Already Hate It

A reader emailed me this old article from CNN Money discussing the concept of the webtop, i.e. that

A killer app no longer requires hundreds of drones slaving away on
millions of lines of code. Three or four engineers and a steady supply
of Red Bull is all it takes to rapidly turn a midnight brainstorm into
a website so hot it melts the servers.

What has changed is the way today's Web-based apps can run almost as
seamlessly as programs used on the desktop, with embedded audio, video,
and drag-and-drop ease of use. Behind this Web-desktop fusion are
technologies like Ajax (asynchronous JavaScript and XML), Macromedia's
Flash, and Ruby on Rails. We'll spare you the technical details;
suffice it to say that these technologies are giving rise to a new
webtop that may one day replace your suite of desktop applications.

I already hate the term "webtop," though I've used it myself in the past. The article goes on to discuss the various webapps that are starting to move into areas once reserved for desktop applications, then lists of a few noteworthy apps, such as 37signals campfire chat client.

The conclusion? We'll all be doing our word processing over the web from now on.

Now I like a little bit of breathless hype as much as the next guy, but this is over the top. Yes, webapps are going to change, but there are certain limitations to the medium that will make the webtop a much tamer place:

  • Reliability and performance - why aren't most desktop apps in corporate environments served up as client/server apps? The technology is there; the kinks of client/server have mostly been worked out. License sharing could save tons of money. The benefits of reliable, centralized storage and ease of collaboration seem pretty obvious. Yet the most we see is networked storage of documents. The reason? Even on a corporate network, performance and reliability are not high enough to make client/server computing for desktop apps attractive.
  • It's more than just CRUD on speed - even after the widespread introduction of the WIMP (Windows, Icons, Menus, Pointing device) environments in the 80's, it took a while for programs to mature beyond GUI versions of Lotus 1-2-3. We didn't see precursors to Photoshop until a few years after the introduction of the Mac. Most webapps today are still glorified CRUD (Create, Read, Update, Delete) engines. Writing the more substantial applications will take a good bit of work, not just a few cans of Red Bull (or Jolt!, sniff).
  • Writely ain't Word - more like Wordpad. If Writely was out as a desktop app, it wouldn't get a second look. Yes, there is a need for a Word-compatible, easier, less bloated, free word processor, but they never seem to make it. Yes, a web based word processor that saves your work more frequently than an occasional submit is kinda cool. But the truly cool part is the collaboration feature of Writely, and honestly, there are other, better solutions for that.
  • Can I Use it Offline? - You're not going to be online all the time. The moment you have desktop versions that can function independent of the server, is it still Ajax or a Javascript/Browser app with save capability?

This hype around Ajax is really starting to remind me of the first dotcom boom (See FauxJAX for a good sendup of this). I had a few venture capitalists back then asking me to look at business plans that added "the web" to their business models. My rule for evaluating these was always the same: what does the web add? Most times it didn't change anything; it was just another marketing channel. For others, like online classifieds, it removed distribution costs. For the social networking type businesses, it made it easier and less costly to jumpstart the network effect.

So, for those going gaga over Ajax, ask yourself, what does Ajax add? If you can't come up with a good answer, odds are it doesn't add a thing.

Scenarios in Product and Project Management

One of the biggest challenges in product and project management is definition and control of scope. By its nature, a design process is a discovery process. As new things are discovered, scope can increase.

One way to manage scope is through scenarios. While a product can have many processes, interactions, screens or flows, at the human level there are likely to be a quantifiable and definable number of scenarios. After all, products and processes are created to enable someone to do something.

By using scenarios as the "buckets" of what a design needs to enable people to do, it's possible to manage scope from this perspective. Any new feature or requirement must fall into one of two categories: It either requires a new scenario, or it's an addition or refinement to an existing scenario. If the scenarios are prioritized, scope impact can be handled by answering the following questions:

- If the new feature requires a new scenario, how high a priority is that scenario?
- If the new feature ties to an existing scenario, does it change the priority of the scenario? Does it change the implementation time for the scenario?

Defining and prioritizing scenarios can be a great help in controlling scope of a project. Scenarios can provide a manageable way of grouping and prioritizing features, innovations and ideas to keep everyone on the same page and the project on-target.

Do You Really Need RETE?

As anyone knows who has been to a Business Rules presentation, it's all about RETE. Who has the best RETE? Which engines are mere pretenders and don't actually have RETE? Which poor slobs have only partially baked version of RETE?

Which brings us to the overwhelming question: what is RETE and why do you need it? By way of answering this questions, we need to go back to what a BRE really is. In most cases, a BRE is an inference engine or production system. According to the Wikipedia, a production system is

...a collection of productions (rules), a working memory
of facts and an algorithm, known as forward chaining, for producing new
facts from old. A rule becomes eligible to "fire" when its conditions
match some set of elements currently in working memory. A conflict
resolution strategy determines which of several eligible rules (the
conflict set) fires next. A condition is a list of symbols which
represent constants, which must be matched exactly; variables which
bind to the thing they match and "<> symbol" which matches a
field not equal to symbol.

What does this actually mean? It means you have conditions on the left hand side (the "IF" part) that if matched trigger some action on the right hand side (the "THEN" part). The right hand side may add, delete or modify facts that will cause new rules to match. For example, I may have a fact that "Bob has a Cat" and the rules that "IF Person has a Cat THEN Person hates Dogs" and "IF Person hates Dogs THEN Person is a Mailman." The first time through my rules I see that "IF Bob has a Cat THEN Bob hates Dogs" fires, and now we have two facts, namely "Bob has a Cat" and "Bob hates Dogs." Now another rule matches, namely "IF Bob hates Dogs THEN Bob is a Mailman," and after firing we have three facts, namely "Bob has a Cat," "Bob hates Dogs," and "Bob is a Mailman." So from the initial fact "Bob has a Cat" we inferred that "Bob is a Mailman." Essentially these inference engines chain syllogisms to come up with new facts.

There's a little more to it than that, such as if we have several rules that match, which one gets triggered first? If we have a bunch of rules that matched on an original set of facts and those facts get changed, then the rules that no longer match have to be kicked out. Still, you get the idea.

If you think of implementing a production system the naive way, you would scan the left hand side for matches, pick the one you like, then rescan the left hand side for matches if the facts changed and repeat. That rescanning of the left hand side gets very expensive if you have a lot of rules and facts. In fact, it was so expensive in the olden days that nobody really used production rules except as objects of academic study.

Enter RETE. Designed by Charles Forgy in 1979, it took the approach of only reevaluating those parts of the left hand side that were effected by a change in the facts. The implementation uses a network (RETE is Latin for "net") to organize terms, conditions and rules so that facts come in the top and rules (or rule retractions) come out the bottom. The RETE algorithm was a huge improvement in performance over the naive approach and made production systems -- or Business Rule Engines -- feasible.

Still, if you aren't doing inferencing, i.e. you are only doing a single pass on the rules or you aren't modifying the facts, there are other algorithms and approaches that in many circumstances blow the doors off of RETE.

If you're problem involves cleaning data ("If the zip code doesn't match the city and state") or checking for eligibility for discounts ("If the customer has purchased more than $5000 in the last 3 months"), then you may in fact not need an inference engine. If, however, you are doing compex financial or clinical reasoning, you may well need inferencing capabilities (Clinical Events->Diagnosis->Recommended Action).

Fortunatley, many commercial vendors support other models of rule evaluation than RETE, so even if you've already invested in one you're not stuck using RETE. Some even support hybrid approaches, i.e. some RETE and some rule chaining. So, before you fall in love with a rule engine because it has the best RETE implementation, ask yourself: do I really need inferencing capabilities in my BRE, or is my problem like most and can make use of a simpler solution?

Topics:

About Pathfinder

  • We design and build extraordinary applications for companies looking to make the next great idea a reality.
  • learn more

Topics

WordPress

Comments about this site: info@pathf.com